Upload files to "/"
This commit is contained in:
105
downloader.py
Normal file
105
downloader.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import requests
|
||||
import os
|
||||
import re
|
||||
|
||||
def clean_filename(title):
|
||||
# Remove quotes and illegal characters
|
||||
title = title.strip("'").strip('"')
|
||||
return re.sub(r'[\\/*?:"<>|]', "", title)
|
||||
|
||||
def download_mp3(url, quality='320'):
|
||||
print(f"\n🔍 Processing: {url}")
|
||||
|
||||
try:
|
||||
# Use Cobalt v9 API to download
|
||||
print("🌐 Requesting download from Cobalt API v9...")
|
||||
|
||||
response = requests.post(
|
||||
'https://api.cobalt.tools/api/v9/process',
|
||||
headers={
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'url': url,
|
||||
'downloadMode': 'audio',
|
||||
'audioFormat': 'mp3'
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
print(f"📡 API Response Status: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
try:
|
||||
error_data = response.json()
|
||||
print(f"❌ Cobalt API error: {error_data}")
|
||||
except:
|
||||
print(f"❌ Cobalt API error: {response.text}")
|
||||
return {"success": False, "error": f"API returned {response.status_code}"}
|
||||
|
||||
data = response.json()
|
||||
print(f"📦 API Response: {data}")
|
||||
|
||||
# Check for errors in response
|
||||
if data.get('status') == 'error':
|
||||
error_msg = data.get('text', 'Unknown error')
|
||||
print(f"❌ Cobalt error: {error_msg}")
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
# Get download URL
|
||||
download_url = data.get('url')
|
||||
if not download_url:
|
||||
print(f"❌ No download URL in response: {data}")
|
||||
return {"success": False, "error": "No download URL received"}
|
||||
|
||||
print(f"📥 Downloading audio...")
|
||||
|
||||
# Download the audio file
|
||||
audio_response = requests.get(download_url, stream=True, timeout=60)
|
||||
|
||||
if audio_response.status_code != 200:
|
||||
print(f"❌ Download failed: {audio_response.status_code}")
|
||||
return {"success": False, "error": f"Download failed with status {audio_response.status_code}"}
|
||||
|
||||
# Try to get filename from Content-Disposition header
|
||||
content_disposition = audio_response.headers.get('Content-Disposition', '')
|
||||
if 'filename=' in content_disposition:
|
||||
filename = content_disposition.split('filename=')[1].strip('"')
|
||||
filename = clean_filename(os.path.splitext(filename)[0])
|
||||
else:
|
||||
# Fallback: extract video ID and use it
|
||||
video_id = url.split('v=')[-1].split('&')[0]
|
||||
filename = f"youtube_{video_id}"
|
||||
|
||||
# Ensure .mp3 extension
|
||||
output_path = f"music/{filename}.mp3"
|
||||
|
||||
# Save the file
|
||||
with open(output_path, 'wb') as f:
|
||||
for chunk in audio_response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
print(f"✅ Success! Saved as: {filename}.mp3")
|
||||
print(" (Hit Refresh in the App)")
|
||||
return {"success": True, "title": filename}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print("❌ Request timed out")
|
||||
return {"success": False, "error": "Request timed out"}
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Network error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not os.path.exists("music"):
|
||||
os.makedirs("music")
|
||||
|
||||
print("--- TECHDJ DOWNLOADER (via Cobalt API) ---")
|
||||
while True:
|
||||
url = input("\n🔗 URL (q to quit): ").strip()
|
||||
if url.lower() == 'q': break
|
||||
if url: download_mp3(url)
|
||||
162
edge_glow_test.html
Normal file
162
edge_glow_test.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Edge Glow Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #0a0a14;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* EDGE GLOW - EXACT COPY FROM style.css lines 28-61 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 99999;
|
||||
border: 3px solid rgba(80, 80, 80, 0.3);
|
||||
}
|
||||
|
||||
body.playing-A::before {
|
||||
border: 15px solid #00f3ff;
|
||||
box-shadow:
|
||||
0 0 80px rgba(0, 243, 255, 1),
|
||||
inset 0 0 80px rgba(0, 243, 255, 0.8);
|
||||
animation: pulse-cyan 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
body.playing-B::before {
|
||||
border: 15px solid #bc13fe;
|
||||
box-shadow:
|
||||
0 0 80px rgba(188, 19, 254, 1),
|
||||
inset 0 0 80px rgba(188, 19, 254, 0.8);
|
||||
animation: pulse-magenta 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
body.playing-A.playing-B::before {
|
||||
border: 15px solid #00f3ff;
|
||||
box-shadow:
|
||||
0 0 80px rgba(0, 243, 255, 1),
|
||||
0 0 120px rgba(188, 19, 254, 1),
|
||||
inset 0 0 80px rgba(0, 243, 255, 0.6),
|
||||
inset 0 0 120px rgba(188, 19, 254, 0.6);
|
||||
animation: pulse-both 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-cyan {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-magenta {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-both {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 20px 40px;
|
||||
margin: 10px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #fff;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="controls">
|
||||
<h1 style="color: #fff;">Edge Glow Test</h1>
|
||||
<button onclick="testDeckA()">Test Deck A (Cyan)</button>
|
||||
<button onclick="testDeckB()">Test Deck B (Magenta)</button>
|
||||
<button onclick="testBoth()">Test Both</button>
|
||||
<button onclick="testOff()">Turn Off</button>
|
||||
<div class="status" id="status">Status: No glow</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function testDeckA() {
|
||||
document.body.className = 'playing-A';
|
||||
document.getElementById('status').textContent = 'Status: Deck A (Cyan) - body class: ' + document.body.className;
|
||||
console.log('Body classes:', document.body.classList.toString());
|
||||
}
|
||||
|
||||
function testDeckB() {
|
||||
document.body.className = 'playing-B';
|
||||
document.getElementById('status').textContent = 'Status: Deck B (Magenta) - body class: ' + document.body.className;
|
||||
console.log('Body classes:', document.body.classList.toString());
|
||||
}
|
||||
|
||||
function testBoth() {
|
||||
document.body.className = 'playing-A playing-B';
|
||||
document.getElementById('status').textContent = 'Status: Both (Cyan + Magenta) - body class: ' + document.body.className;
|
||||
console.log('Body classes:', document.body.classList.toString());
|
||||
}
|
||||
|
||||
function testOff() {
|
||||
document.body.className = '';
|
||||
document.getElementById('status').textContent = 'Status: No glow - body class: (empty)';
|
||||
console.log('Body classes:', document.body.classList.toString());
|
||||
}
|
||||
|
||||
// Auto-test on load
|
||||
setTimeout(() => {
|
||||
console.log('=== EDGE GLOW TEST ===');
|
||||
console.log('If you see a CYAN border around the screen, the glow is working!');
|
||||
testDeckA();
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
473
index.html
Normal file
473
index.html
Normal file
@@ -0,0 +1,473 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>TechDJ Pro</title>
|
||||
<link rel="stylesheet" href="style.css?v=7.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="start-overlay">
|
||||
<h1 class="overlay-title">TECHDJ PROTOCOL</h1>
|
||||
<button id="start-btn" onclick="initSystem()">INITIALIZE SYSTEM</button>
|
||||
<p style="color:#666; margin-top:20px; font-family:'Rajdhani'">v2.0 // NEON CORE</p>
|
||||
</div>
|
||||
|
||||
<!-- Landscape Orientation Prompt -->
|
||||
<div class="landscape-prompt" id="landscape-prompt">
|
||||
<div class="rotate-icon">📱→🔄</div>
|
||||
<h2>OPTIMAL ORIENTATION</h2>
|
||||
<p>For the best DJ experience, please rotate your device to <strong>landscape mode</strong> (sideways).</p>
|
||||
<p style="font-size: 0.9rem; color: #888;">Both decks and the crossfader will be visible simultaneously.</p>
|
||||
<button onclick="dismissLandscapePrompt()">GOT IT</button>
|
||||
</div>
|
||||
|
||||
<!-- MAIN APP CONTAINER -->
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Mobile Tabs -->
|
||||
<nav class="mobile-tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('library')">
|
||||
<span class="tab-icon">📁</span>
|
||||
<span>LIBRARY</span>
|
||||
</button>
|
||||
<button class="tab-btn" onclick="switchTab('deck-A')">
|
||||
<span class="tab-icon">💿</span>
|
||||
<span>DECK A</span>
|
||||
</button>
|
||||
<button class="tab-btn" onclick="switchTab('deck-B')">
|
||||
<span class="tab-icon">💿</span>
|
||||
<span>DECK B</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 1. LEFT: LIBRARY -->
|
||||
<section class="library-section">
|
||||
<div class="lib-header">
|
||||
<input type="text" id="lib-search" placeholder="🔍 FILTER LIBRARY..." onkeyup="filterLibrary()">
|
||||
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">🔄</button>
|
||||
</div>
|
||||
|
||||
<!-- YouTube Search -->
|
||||
<div class="youtube-search-container">
|
||||
<input type="text" id="youtube-search" placeholder="🎵 SEARCH YOUTUBE..."
|
||||
onkeyup="handleYouTubeSearch(this.value)">
|
||||
<div id="youtube-results" class="youtube-results"></div>
|
||||
</div>
|
||||
|
||||
<div id="library-list" class="library-list">
|
||||
<!-- Tracks go here -->
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 2. MIDDLE: DECK A -->
|
||||
<div class="deck" id="deck-A">
|
||||
<div class="deck-header">
|
||||
<span class="deck-title title-a">DECK A</span>
|
||||
<span class="track-display" id="display-A">NO TRACK LOADED</span>
|
||||
</div>
|
||||
|
||||
<!-- Waveform Display -->
|
||||
<div class="waveform-container">
|
||||
<canvas id="waveform-A" class="waveform-canvas"></canvas>
|
||||
<div class="playhead" id="playhead-A"></div>
|
||||
<div class="time-display">
|
||||
<span id="time-current-A">0:00</span> / <span id="time-total-A">0:00</span>
|
||||
<span class="bpm-display" id="bpm-A"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disk-container">
|
||||
<div class="dj-disk" id="disk-A">
|
||||
<div class="disk-label">A</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="viz-A" width="400" height="100"></canvas>
|
||||
|
||||
<!-- Hot Cues -->
|
||||
<div class="hot-cues">
|
||||
<button class="cue-btn cue-1" onclick="handleCue('A', 1)"
|
||||
oncontextmenu="clearCue('A', 1); return false;">CUE 1</button>
|
||||
<button class="cue-btn cue-2" onclick="handleCue('A', 2)"
|
||||
oncontextmenu="clearCue('A', 2); return false;">CUE 2</button>
|
||||
<button class="cue-btn cue-3" onclick="handleCue('A', 3)"
|
||||
oncontextmenu="clearCue('A', 3); return false;">CUE 3</button>
|
||||
<button class="cue-btn cue-4" onclick="handleCue('A', 4)"
|
||||
oncontextmenu="clearCue('A', 4); return false;">CUE 4</button>
|
||||
</div>
|
||||
|
||||
<!-- Loop Controls -->
|
||||
<div class="loop-controls">
|
||||
<button class="loop-btn" onclick="setLoop('A', 'in')">LOOP IN</button>
|
||||
<button class="loop-btn" onclick="setLoop('A', 'out')">LOOP OUT</button>
|
||||
<button class="loop-btn" onclick="setLoop('A', 'exit')">EXIT</button>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Loop Beat Controls -->
|
||||
<div class="auto-loop-controls">
|
||||
<label class="auto-loop-label">AUTO-LOOP</label>
|
||||
<div class="auto-loop-buttons">
|
||||
<button class="auto-loop-btn" data-beats="8" onclick="setAutoLoop('A', 8)">8</button>
|
||||
<button class="auto-loop-btn" data-beats="4" onclick="setAutoLoop('A', 4)">4</button>
|
||||
<button class="auto-loop-btn" data-beats="2" onclick="setAutoLoop('A', 2)">2</button>
|
||||
<button class="auto-loop-btn" data-beats="1" onclick="setAutoLoop('A', 1)">1</button>
|
||||
<button class="auto-loop-btn" data-beats="0.5" onclick="setAutoLoop('A', 0.5)">1/2</button>
|
||||
<button class="auto-loop-btn" data-beats="0.25" onclick="setAutoLoop('A', 0.25)">1/4</button>
|
||||
<button class="auto-loop-btn" data-beats="0.125" onclick="setAutoLoop('A', 0.125)">1/8</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-group">
|
||||
<input type="text" class="search-input" id="search-input-A" placeholder="PASTE YOUTUBE URL...">
|
||||
<div class="download-controls">
|
||||
<select id="quality-A" class="quality-selector">
|
||||
<option value="128">128kbps</option>
|
||||
<option value="192">192kbps</option>
|
||||
<option value="320" selected>320kbps</option>
|
||||
</select>
|
||||
<button class="download-btn" onclick="downloadFromPanel('A')">⬇ DOWNLOAD</button>
|
||||
</div>
|
||||
<div id="download-status-A" class="download-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls-grid">
|
||||
<!-- Volume Fader -->
|
||||
<div class="fader-group">
|
||||
<label>VOLUME</label>
|
||||
<input type="range" orient="vertical" class="volume-fader" min="0" max="100" value="80"
|
||||
oninput="changeVolume('A', this.value)">
|
||||
</div>
|
||||
|
||||
<!-- EQ -->
|
||||
<div class="eq-container">
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('A', 'high', this.value)"><label>HI</label></div>
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('A', 'mid', this.value)"><label>MID</label></div>
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('A', 'low', this.value)"><label>LO</label></div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-knobs">
|
||||
<div class="filter-knob">
|
||||
<label>LOW-PASS</label>
|
||||
<input type="range" class="filter-slider filter-lp" min="0" max="100" value="100"
|
||||
oninput="changeFilter('A', 'lowpass', this.value)">
|
||||
</div>
|
||||
<div class="filter-knob">
|
||||
<label>HIGH-PASS</label>
|
||||
<input type="range" class="filter-slider filter-hp" min="0" max="100" value="0"
|
||||
oninput="changeFilter('A', 'highpass', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pitch/Tempo -->
|
||||
<div class="fader-group">
|
||||
<label>PITCH / TEMPO</label>
|
||||
<input type="range" class="speed-slider" min="0.5" max="1.5" step="0.01" value="1"
|
||||
oninput="changeSpeed('A', this.value)" ondblclick="this.value=1; changeSpeed('A', 1);">
|
||||
<div class="pitch-bend-buttons">
|
||||
<button class="pitch-bend" onmousedown="pitchBend('A', -0.05)" onmouseup="pitchBend('A', 0)"
|
||||
onmouseleave="pitchBend('A', 0)">-</button>
|
||||
<button class="pitch-bend" onmousedown="pitchBend('A', 0.05)" onmouseup="pitchBend('A', 0)"
|
||||
onmouseleave="pitchBend('A', 0)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="transport">
|
||||
<button class="big-btn play-btn" onclick="playDeck('A')">PLAY</button>
|
||||
<button class="big-btn pause-btn" onclick="pauseDeck('A')">PAUSE</button>
|
||||
<button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</button>
|
||||
<button class="big-btn reset-btn" onclick="resetDeck('A')" title="Reset all settings to default">🔄
|
||||
RESET</button>
|
||||
</div>
|
||||
|
||||
<!-- QUEUE for Deck A -->
|
||||
<div class="queue-panel" id="queue-panel-A">
|
||||
<div class="queue-header">
|
||||
<span class="queue-title">📋 QUEUE A</span>
|
||||
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">🗑️</button>
|
||||
</div>
|
||||
<div class="queue-list" id="queue-list-A">
|
||||
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. RIGHT: DECK B -->
|
||||
<div class="deck" id="deck-B">
|
||||
<div class="deck-header">
|
||||
<span class="deck-title title-b">DECK B</span>
|
||||
<span class="track-display" id="display-B">NO TRACK LOADED</span>
|
||||
</div>
|
||||
|
||||
<!-- Waveform Display -->
|
||||
<div class="waveform-container">
|
||||
<canvas id="waveform-B" class="waveform-canvas"></canvas>
|
||||
<div class="playhead" id="playhead-B"></div>
|
||||
<div class="time-display">
|
||||
<span id="time-current-B">0:00</span> / <span id="time-total-B">0:00</span>
|
||||
<span class="bpm-display" id="bpm-B"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disk-container">
|
||||
<div class="dj-disk" id="disk-B">
|
||||
<div class="disk-label">B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="viz-B" width="400" height="100"></canvas>
|
||||
|
||||
<!-- Hot Cues -->
|
||||
<div class="hot-cues">
|
||||
<button class="cue-btn cue-1" onclick="handleCue('B', 1)"
|
||||
oncontextmenu="clearCue('B', 1); return false;">CUE 1</button>
|
||||
<button class="cue-btn cue-2" onclick="handleCue('B', 2)"
|
||||
oncontextmenu="clearCue('B', 2); return false;">CUE 2</button>
|
||||
<button class="cue-btn cue-3" onclick="handleCue('B', 3)"
|
||||
oncontextmenu="clearCue('B', 3); return false;">CUE 3</button>
|
||||
<button class="cue-btn cue-4" onclick="handleCue('B', 4)"
|
||||
oncontextmenu="clearCue('B', 4); return false;">CUE 4</button>
|
||||
</div>
|
||||
|
||||
<!-- Loop Controls -->
|
||||
<div class="loop-controls">
|
||||
<button class="loop-btn" onclick="setLoop('B', 'in')">LOOP IN</button>
|
||||
<button class="loop-btn" onclick="setLoop('B', 'out')">LOOP OUT</button>
|
||||
<button class="loop-btn" onclick="setLoop('B', 'exit')">EXIT</button>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Loop Beat Controls -->
|
||||
<div class="auto-loop-controls">
|
||||
<label class="auto-loop-label">AUTO-LOOP</label>
|
||||
<div class="auto-loop-buttons">
|
||||
<button class="auto-loop-btn" data-beats="8" onclick="setAutoLoop('B', 8)">8</button>
|
||||
<button class="auto-loop-btn" data-beats="4" onclick="setAutoLoop('B', 4)">4</button>
|
||||
<button class="auto-loop-btn" data-beats="2" onclick="setAutoLoop('B', 2)">2</button>
|
||||
<button class="auto-loop-btn" data-beats="1" onclick="setAutoLoop('B', 1)">1</button>
|
||||
<button class="auto-loop-btn" data-beats="0.5" onclick="setAutoLoop('B', 0.5)">1/2</button>
|
||||
<button class="auto-loop-btn" data-beats="0.25" onclick="setAutoLoop('B', 0.25)">1/4</button>
|
||||
<button class="auto-loop-btn" data-beats="0.125" onclick="setAutoLoop('B', 0.125)">1/8</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-group">
|
||||
<input type="text" class="search-input" id="search-input-B" placeholder="PASTE YOUTUBE URL...">
|
||||
<div class="download-controls">
|
||||
<select id="quality-B" class="quality-selector">
|
||||
<option value="128">128kbps</option>
|
||||
<option value="192">192kbps</option>
|
||||
<option value="320" selected>320kbps</option>
|
||||
</select>
|
||||
<button class="download-btn" onclick="downloadFromPanel('B')">⬇ DOWNLOAD</button>
|
||||
</div>
|
||||
<div id="download-status-B" class="download-status"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="controls-grid">
|
||||
<!-- Volume Fader -->
|
||||
<div class="fader-group">
|
||||
<label>VOLUME</label>
|
||||
<input type="range" orient="vertical" class="volume-fader" min="0" max="100" value="80"
|
||||
oninput="changeVolume('B', this.value)">
|
||||
</div>
|
||||
|
||||
<!-- EQ -->
|
||||
<div class="eq-container">
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('B', 'high', this.value)"><label>HI</label></div>
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('B', 'mid', this.value)"><label>MID</label></div>
|
||||
<div class="eq-band"><input type="range" orient="vertical" min="-20" max="20" value="0"
|
||||
oninput="changeEQ('B', 'low', this.value)"><label>LO</label></div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-knobs">
|
||||
<div class="filter-knob">
|
||||
<label>LOW-PASS</label>
|
||||
<input type="range" class="filter-slider filter-lp" min="0" max="100" value="100"
|
||||
oninput="changeFilter('B', 'lowpass', this.value)">
|
||||
</div>
|
||||
<div class="filter-knob">
|
||||
<label>HIGH-PASS</label>
|
||||
<input type="range" class="filter-slider filter-hp" min="0" max="100" value="0"
|
||||
oninput="changeFilter('B', 'highpass', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pitch/Tempo -->
|
||||
<div class="fader-group">
|
||||
<label>PITCH / TEMPO</label>
|
||||
<input type="range" class="speed-slider" min="0.5" max="1.5" step="0.01" value="1"
|
||||
oninput="changeSpeed('B', this.value)" ondblclick="this.value=1; changeSpeed('B', 1);">
|
||||
<div class="pitch-bend-buttons">
|
||||
<button class="pitch-bend" onmousedown="pitchBend('B', -0.05)" onmouseup="pitchBend('B', 0)"
|
||||
onmouseleave="pitchBend('B', 0)">-</button>
|
||||
<button class="pitch-bend" onmousedown="pitchBend('B', 0.05)" onmouseup="pitchBend('B', 0)"
|
||||
onmouseleave="pitchBend('B', 0)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="transport">
|
||||
<button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button>
|
||||
<button class="big-btn pause-btn" onclick="pauseDeck('B')">PAUSE</button>
|
||||
<button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</button>
|
||||
<button class="big-btn reset-btn" onclick="resetDeck('B')" title="Reset all settings to default">🔄
|
||||
RESET</button>
|
||||
</div>
|
||||
|
||||
<!-- QUEUE for Deck B -->
|
||||
<div class="queue-panel" id="queue-panel-B">
|
||||
<div class="queue-header">
|
||||
<span class="queue-title">📋 QUEUE B</span>
|
||||
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">🗑️</button>
|
||||
</div>
|
||||
<div class="queue-list" id="queue-list-B">
|
||||
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. BOTTOM: CROSSFADER -->
|
||||
<div class="mixer-section">
|
||||
<input type="range" class="xfader" id="crossfader" min="0" max="100" value="50"
|
||||
oninput="updateCrossfader(this.value)">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Live Streaming Panel -->
|
||||
<div class="streaming-panel" id="streaming-panel">
|
||||
<div class="streaming-header">
|
||||
<span>📡 LIVE STREAM</span>
|
||||
<button class="close-streaming" onclick="toggleStreamingPanel()">✕</button>
|
||||
</div>
|
||||
<div class="streaming-content">
|
||||
<div class="broadcast-controls">
|
||||
<button class="broadcast-btn" id="broadcast-btn" onclick="toggleBroadcast()">
|
||||
<span class="broadcast-icon">🔴</span>
|
||||
<span id="broadcast-text">START BROADCAST</span>
|
||||
</button>
|
||||
<div class="broadcast-status" id="broadcast-status">Offline</div>
|
||||
</div>
|
||||
|
||||
<div class="listener-info">
|
||||
<div class="listener-count">
|
||||
<span class="count-icon">👂</span>
|
||||
<span id="listener-count">0</span>
|
||||
<span class="count-label">Listeners</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stream-url-section">
|
||||
<label>Share this URL:</label>
|
||||
<div class="url-copy-group">
|
||||
<input type="text" id="stream-url" readonly value="http://localhost:5000?listen=true">
|
||||
<button onclick="copyStreamUrl()" class="copy-btn">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stream-settings">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-start-stream" onchange="toggleAutoStream(this.checked)">
|
||||
Auto-start on play
|
||||
</label>
|
||||
|
||||
<div class="quality-selector-group">
|
||||
<label for="stream-quality">Stream Quality:</label>
|
||||
<select id="stream-quality" class="stream-quality-select">
|
||||
<option value="128">High (128kbps)</option>
|
||||
<option value="96" selected>Medium (96kbps)</option>
|
||||
<option value="64">Low (64kbps)</option>
|
||||
<option value="48">Very Low (48kbps)</option>
|
||||
<option value="32">Minimum (32kbps)</option>
|
||||
</select>
|
||||
<span class="quality-hint">Lower = more stable on poor connections</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Listener Mode (Hidden by default) -->
|
||||
<div class="listener-mode" id="listener-mode" style="display: none;">
|
||||
<div class="listener-header">
|
||||
<h1>🎧 TECHDJ LIVE</h1>
|
||||
<div class="live-indicator">
|
||||
<span class="pulse-dot"></span>
|
||||
<span>LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listener-content">
|
||||
<div class="now-playing" id="listener-now-playing">Waiting for stream...</div>
|
||||
|
||||
<!-- Enable Audio Button (shown when autoplay is blocked) -->
|
||||
<button class="enable-audio-btn" id="enable-audio-btn" style="display: none;"
|
||||
onclick="enableListenerAudio()">
|
||||
<span class="audio-icon">🔊</span>
|
||||
<span class="audio-text">ENABLE AUDIO</span>
|
||||
<span class="audio-subtitle">Click to start listening</span>
|
||||
</button>
|
||||
|
||||
<div class="volume-control">
|
||||
<label>🔊 Volume</label>
|
||||
<input type="range" id="listener-volume" min="0" max="100" value="80"
|
||||
oninput="setListenerVolume(this.value)">
|
||||
</div>
|
||||
<div class="connection-status" id="connection-status">Connecting...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<!-- Settings Panel -->
|
||||
<div class="settings-panel" id="settings-panel">
|
||||
<div class="settings-header">
|
||||
<span>⚙️ SETTINGS</span>
|
||||
<button class="close-settings" onclick="toggleSettings()">✕</button>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<div class="setting-item"><label><input type="checkbox" id="repeat-A"
|
||||
onchange="toggleRepeat('A', this.checked)">🔁 Repeat Deck A</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="repeat-B"
|
||||
onchange="toggleRepeat('B', this.checked)"><EFBFBD><EFBFBD> Repeat Deck B</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="auto-mix"
|
||||
onchange="toggleAutoMix(this.checked)">🎛️ Auto-Crossfade</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="shuffle-mode"
|
||||
onchange="toggleShuffle(this.checked)">🔀 Shuffle Library</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="quantize"
|
||||
onchange="toggleQuantize(this.checked)">📐 Quantize</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="auto-play" checked
|
||||
onchange="toggleAutoPlay(this.checked)">▶️ Auto-play next</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="glow-A"
|
||||
onchange="updateManualGlow('A', this.checked)">✨ Glow Deck A (Cyan)</label></div>
|
||||
<div class="setting-item"><label><input type="checkbox" id="glow-B"
|
||||
onchange="updateManualGlow('B', this.checked)">✨ Glow Deck B (Magenta)</label></div>
|
||||
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
|
||||
<label>✨ Glow Intensity</label>
|
||||
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
|
||||
oninput="updateGlowIntensity(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">⌨️</button>
|
||||
<button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">📡</button>
|
||||
<button class="upload-btn" onclick="document.getElementById('file-upload').click()" title="Upload MP3">📁</button>
|
||||
<input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none"
|
||||
onchange="handleFileUpload(event)">
|
||||
<button class="settings-btn" onclick="toggleSettings()">⚙️</button>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# TechDJ Requirements
|
||||
flask
|
||||
flask-socketio
|
||||
yt-dlp
|
||||
eventlet
|
||||
python-dotenv
|
||||
|
||||
Reference in New Issue
Block a user