remove server-sid history

This commit is contained in:
2026-05-05 20:27:42 +02:00
parent 2f413766fd
commit 353f9b9e9b
4 changed files with 1121 additions and 603 deletions
+3 -65
View File
@@ -23,7 +23,6 @@ import subprocess # NEU: Für FFmpeg Aufruf
import traceback # NEU: Für detaillierte Fehlermeldungen import traceback # NEU: Für detaillierte Fehlermeldungen
# --- Konstanten --- # --- Konstanten ---
HISTORY_FILE = "download_history.json"
STATS_FILE = "stats.json" STATS_FILE = "stats.json"
RANDOM_NAME_LENGTH = 4 RANDOM_NAME_LENGTH = 4
MAX_FILENAME_RETRIES = 10 MAX_FILENAME_RETRIES = 10
@@ -72,7 +71,6 @@ if env_loaded_successfully: logging.info(f".env Datei gefunden und geladen von:
else: logging.warning(".env Datei nicht gefunden...") else: logging.warning(".env Datei nicht gefunden...")
# --- Konfiguration aus .env lesen --- # --- Konfiguration aus .env lesen ---
ENABLE_HISTORY = os.getenv('ENABLE_HISTORY', 'true').lower() == 'true'
try: try:
MAX_WORKERS = int(os.getenv('MAX_WORKERS', '1')) MAX_WORKERS = int(os.getenv('MAX_WORKERS', '1'))
if MAX_WORKERS < 1: if MAX_WORKERS < 1:
@@ -84,7 +82,6 @@ except ValueError:
if MAX_WORKERS <= 0: MAX_WORKERS = 1 # Sicherstellen, dass mindestens 1 Worker läuft if MAX_WORKERS <= 0: MAX_WORKERS = 1 # Sicherstellen, dass mindestens 1 Worker läuft
logging.info(f"Verlauf aktiviert: {ENABLE_HISTORY}")
logging.info(f"Maximale Worker-Threads (für Hintergrundverarbeitung): {MAX_WORKERS}") logging.info(f"Maximale Worker-Threads (für Hintergrundverarbeitung): {MAX_WORKERS}")
# --- Flask App Initialisierung --- # --- Flask App Initialisierung ---
@@ -455,42 +452,7 @@ def upload_to_s3(job_id, file_path, object_name, file_extension, bucket_name, aw
update_status(job_id, error=error_msg, running=False) update_status(job_id, error=error_msg, running=False)
return False return False
# --- History Funktionen (Backend - unverändert) --- # --- Statistik Funktionen ---
def load_history():
if not ENABLE_HISTORY: return []
try:
if os.path.exists(HISTORY_FILE):
if os.path.getsize(HISTORY_FILE) == 0: logging.warning(f"{HISTORY_FILE} ist leer."); return []
with open(HISTORY_FILE, 'r', encoding='utf-8') as f: history = json.load(f)
return history if isinstance(history, list) else []
else: return []
except json.JSONDecodeError as e: logging.error(f"Fehler Laden History (JSON ungültig): {e}."); return []
except Exception as e: logging.error(f"Fehler Laden History (Allgemein): {e}"); return []
def save_history(history_data):
if not ENABLE_HISTORY: return True
try:
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history_data, f, indent=4, ensure_ascii=False)
logging.info(f"History gespeichert: {HISTORY_FILE}")
return True
except Exception as e: logging.error(f"Fehler Speichern History: {e}"); return False
def add_history_entry(platform, title, source_url, s3_url):
if not ENABLE_HISTORY: return True
history = load_history()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {"timestamp": timestamp, "platform": platform, "title": title, "source_url": source_url, "s3_url": s3_url}
history.insert(0, entry)
return save_history(history)
def clear_history_file():
if not ENABLE_HISTORY: return True
try:
if os.path.exists(HISTORY_FILE): os.remove(HISTORY_FILE); logging.info("History-Datei gelöscht.")
return True
except Exception as e: logging.error(f"Fehler Löschen History-Datei: {e}"); return False
# --- Statistik Funktionen (Backend - unverändert) ---
def load_stats(): def load_stats():
default_stats = {'total_jobs': 0, 'successful_jobs': 0, 'total_duration_seconds': 0.0, 'total_size_bytes': 0} default_stats = {'total_jobs': 0, 'successful_jobs': 0, 'total_duration_seconds': 0.0, 'total_size_bytes': 0}
try: try:
@@ -633,10 +595,6 @@ def run_download_upload_task(job_id, url, platform, format_preference, mp3_bitra
update_status(job_id, message="Abgeschlossen! (Keine Public URL Base konfiguriert)") update_status(job_id, message="Abgeschlossen! (Keine Public URL Base konfiguriert)")
logging.warning(f"[{job_id}] Öffentliche URL kann nicht angezeigt werden (S3_PUBLIC_URL_BASE fehlt).") logging.warning(f"[{job_id}] Öffentliche URL kann nicht angezeigt werden (S3_PUBLIC_URL_BASE fehlt).")
if not add_history_entry(platform, track_title, url, final_s3_url_for_history):
logging.warning(f"[{job_id}] Konnte Eintrag nicht zur History hinzufügen.")
update_status(job_id, log_entry="WARNUNG: Konnte Eintrag nicht zur History hinzufügen.")
except Exception as e: except Exception as e:
logging.exception(f"[{job_id}] Unerwarteter Fehler im Hauptverarbeitungsblock für URL {url}:") logging.exception(f"[{job_id}] Unerwarteter Fehler im Hauptverarbeitungsblock für URL {url}:")
final_error_message = f"Unerwarteter Verarbeitungsfehler: {strip_ansi_codes(str(e))}" final_error_message = f"Unerwarteter Verarbeitungsfehler: {strip_ansi_codes(str(e))}"
@@ -757,7 +715,7 @@ def index():
if not isinstance(job_statuses, type(manager.dict())): if not isinstance(job_statuses, type(manager.dict())):
job_statuses = manager.dict() job_statuses = manager.dict()
logging.warning("job_statuses wurde neu initialisiert (wahrscheinlich nach Reload).") logging.warning("job_statuses wurde neu initialisiert (wahrscheinlich nach Reload).")
return render_template('index.html', history_enabled=ENABLE_HISTORY) return render_template('index.html')
@app.route('/start_download', methods=['POST']) @app.route('/start_download', methods=['POST'])
def start_download(): def start_download():
@@ -787,14 +745,6 @@ def start_download():
if platform == "Twitter": error_msg += " Stelle sicher, dass es ein Tweet-Link ist (enthält /status/)." if platform == "Twitter": error_msg += " Stelle sicher, dass es ein Tweet-Link ist (enthält /status/)."
return jsonify({"error": error_msg}), 400 return jsonify({"error": error_msg}), 400
if ENABLE_HISTORY:
history = load_history()
for entry in history:
entry_url = entry.get('source_url') or entry.get('soundcloud_url')
if entry_url == url:
entry_platform = entry.get('platform', 'Unbekannt')
return jsonify({"error": f"Dieser Link ({entry_platform}) wurde bereits verarbeitet (Verlauf aktiv)."}), 400
access_key = os.getenv('AWS_ACCESS_KEY_ID'); secret_key = os.getenv('AWS_SECRET_ACCESS_KEY') access_key = os.getenv('AWS_ACCESS_KEY_ID'); secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
bucket_name = os.getenv('AWS_S3_BUCKET_NAME'); region_name = os.getenv('AWS_REGION') bucket_name = os.getenv('AWS_S3_BUCKET_NAME'); region_name = os.getenv('AWS_REGION')
endpoint_url = os.getenv('S3_ENDPOINT_URL') endpoint_url = os.getenv('S3_ENDPOINT_URL')
@@ -845,18 +795,6 @@ def get_status():
current_status_copy["logs"] = list(current_status_copy["logs"]) current_status_copy["logs"] = list(current_status_copy["logs"])
return jsonify(current_status_copy) return jsonify(current_status_copy)
@app.route('/history')
def get_history():
history = load_history()
return jsonify(history)
@app.route('/clear_history', methods=['POST'])
def clear_history_route():
if clear_history_file():
return jsonify({"message": "Verlauf gelöscht (falls aktiviert)."}), 200
else:
return jsonify({"error": "Fehler beim Löschen des Verlaufs."}), 500
@app.route('/stats') @app.route('/stats')
def get_stats(): def get_stats():
stats_data = load_stats() stats_data = load_stats()
@@ -942,7 +880,7 @@ if __name__ == '__main__':
else: print("\nINFO: FFmpeg gefunden.\n") else: print("\nINFO: FFmpeg gefunden.\n")
print(f"\nFlask App startet (lokaler Modus)..."); print(f"\nFlask App startet (lokaler Modus)...");
print(f"Download-Verzeichnis: {DOWNLOAD_DIR}") print(f"Download-Verzeichnis: {DOWNLOAD_DIR}")
print(f"Verlauf aktiviert: {ENABLE_HISTORY}") print(f"History: Browser localStorage only")
print(f"Öffne http://127.0.0.1:5000 oder http://<Deine-IP>:5000 im Browser.") print(f"Öffne http://127.0.0.1:5000 oder http://<Deine-IP>:5000 im Browser.")
print("(Beende mit STRG+C)\n") print("(Beende mit STRG+C)\n")
app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False) app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)
+278 -224
View File
@@ -1,304 +1,358 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dom = { const dom = {
// Form & Main form: document.getElementById('upload-form'),
form: document.getElementById('upload-form'), urlInput: document.getElementById('url'),
urlInput: document.getElementById('url'), platformInput: document.getElementById('input-platform'),
submitBtn: document.getElementById('submit-button'), detectionArea: document.getElementById('detection-area'),
platformInput: document.getElementById('input-platform'), detectedText: document.getElementById('detected-text'),
detectedIcon: document.getElementById('detected-icon'),
// Detection UI platformPulse: document.getElementById('platform-pulse'),
detectionArea: document.getElementById('detection-area'), platformBadge: document.getElementById('platform-badge'),
detectedText: document.getElementById('detected-text'), urlIcon: document.getElementById('url-icon'),
detectedIcon: document.getElementById('detected-icon'), inputWrapper: document.getElementById('input-wrapper'),
urlIcon: document.getElementById('url-icon'), ytOptions: document.getElementById('youtube-options'),
codecSection: document.getElementById('codec-options-section'),
// Options ytRadios: document.querySelectorAll('input[name="yt_format"]'),
optionsContainer: document.getElementById('options-container'), mp3Select: document.getElementById('mp3_bitrate'),
ytOptions: document.getElementById('youtube-options'), mp4Select: document.getElementById('mp4_quality'),
codecSection: document.getElementById('codec-options-section'), codecSwitch: document.getElementById('codec-switch'),
ytRadios: document.querySelectorAll('input[name="yt_format"]'), codecPref: document.getElementById('codec_preference'),
mp3Select: document.getElementById('mp3_bitrate'), processView: document.getElementById('process-view'),
mp4Select: document.getElementById('mp4_quality'), resultView: document.getElementById('result-view'),
codecSwitch: document.getElementById('codec-switch'), formContainer: document.querySelector('.form-container'),
codecPref: document.getElementById('codec_preference'), statusMsg: document.getElementById('status-message'),
progressBar: document.getElementById('progress-bar'),
// Views progressPct: document.getElementById('progress-pct'),
processView: document.getElementById('process-view'), logContent: document.getElementById('pseudo-log-content'),
resultView: document.getElementById('result-view'), resultUrl: document.getElementById('result-url'),
formContainer: document.querySelector('.form-container'), errorMsg: document.getElementById('error-message'),
historyTbody: document.querySelector('#history-table tbody'),
// Progress & Logs clearHistoryBtn:document.getElementById('clear-history-button'),
statusMsg: document.getElementById('status-message'),
progressBar: document.getElementById('progress-bar'),
logContent: document.getElementById('pseudo-log-content'),
// Result
resultUrl: document.getElementById('result-url'),
errorMsg: document.getElementById('error-message'),
// History
historyTableBody: document.querySelector('#history-table tbody'),
clearHistoryBtn: document.getElementById('clear-history-button')
}; };
let currentJobId = null; let jobId = null;
let pollingInterval = null; let pollTimer = null;
let pseudoLogInterval = null; let pseudoTimer = null;
let lastLogIndex = 0;
let realLogsActive = false;
const platforms = [ // ---- Platform definitions ----
{ name: 'SoundCloud', pattern: /soundcloud\.com/, icon: 'fa-soundcloud', color: '#ff5500' }, const PLATFORMS = [
{ name: 'YouTube', pattern: /(youtube\.com|youtu\.be)/, icon: 'fa-youtube', color: '#ff0000' }, { name:'SoundCloud', pattern:/soundcloud\.com/, icon:'fa-soundcloud', color:'#ff5500', rgb:'255,85,0' },
{ name: 'TikTok', pattern: /tiktok\.com/, icon: 'fa-tiktok', color: '#fe2c55' }, { name:'YouTube', pattern:/(youtube\.com|youtu\.be)/,icon:'fa-youtube', color:'#ff0000', rgb:'255,0,0' },
{ name: 'Instagram', pattern: /instagram\.com/, icon: 'fa-instagram', color: '#E1306C' }, { name:'TikTok', pattern:/tiktok\.com/, icon:'fa-tiktok', color:'#fe2c55', rgb:'254,44,85' },
{ name: 'Twitter', pattern: /(twitter\.com|x\.com)/, icon: 'fa-x-twitter', color: '#fff' } { name:'Instagram', pattern:/instagram\.com/, icon:'fa-instagram', color:'#E1306C', rgb:'225,48,108' },
{ name:'Twitter', pattern:/(twitter\.com|x\.com)/, icon:'fa-x-twitter', color:'#1d9bf0', rgb:'29,155,240' },
]; ];
const pseudoLogs = [ const PSEUDO_LOGS = [
"Connecting to media node...", 'Connecting to media node...',
"Handshaking with API...", 'Handshaking with API endpoint...',
"Resolving stream URL...", 'Resolving stream metadata...',
"Allocating buffer...", 'Allocating download buffer...',
"Starting download stream...", 'Starting download stream...',
"Processing data chunks...", 'Processing data chunks...',
"Verifying integrity...", 'Verifying file integrity...',
"Optimizing container...", 'Optimizing audio/video container...',
"Finalizing upload..." 'Preparing upload to storage...',
'Syncing with remote bucket...',
]; ];
// ---- Init ----
function init() { function init() {
setupEvents(); bindEvents();
loadHistory(); renderHistory();
} }
function setupEvents() { function bindEvents() {
dom.urlInput.addEventListener('input', handleUrlInput); dom.urlInput.addEventListener('input', onUrlChange);
dom.form.addEventListener('submit', handleSubmit); dom.form.addEventListener('submit', onSubmit);
dom.ytRadios.forEach(r => r.addEventListener('change', updateQualitySelect));
dom.ytRadios.forEach(r => r.addEventListener('change', updateYtOptions)); if (dom.codecSwitch) {
if(dom.codecSwitch) {
dom.codecSwitch.addEventListener('change', e => { dom.codecSwitch.addEventListener('change', e => {
dom.codecPref.value = e.target.checked ? 'h264' : 'original'; dom.codecPref.value = e.target.checked ? 'h264' : 'original';
}); });
} }
if (dom.clearHistoryBtn) {
if(dom.clearHistoryBtn) dom.clearHistoryBtn.addEventListener('click', clearHistory); dom.clearHistoryBtn.addEventListener('click', clearHistory);
}
// --- Detection ---
function handleUrlInput() {
const url = dom.urlInput.value.trim();
const detected = platforms.find(p => p.pattern.test(url));
if (detected) {
dom.platformInput.value = detected.name;
dom.detectedText.textContent = detected.name + " detected";
dom.detectedIcon.className = `fab ${detected.icon}`;
dom.detectionArea.style.opacity = '1';
dom.detectionArea.style.transform = 'translateY(0)';
// Icon in Input
dom.urlIcon.className = `fab ${detected.icon}`;
dom.urlIcon.style.color = detected.color;
showOptions(detected.name);
} else {
if(url.length === 0) {
dom.detectionArea.style.opacity = '0';
dom.detectionArea.style.transform = 'translateY(-10px)';
dom.urlIcon.className = 'fas fa-link';
dom.urlIcon.style.color = '';
}
// Keep options hidden if no match
} }
} }
function showOptions(platform) { // ---- URL detection ----
function onUrlChange() {
const val = dom.urlInput.value.trim();
const match = PLATFORMS.find(p => p.pattern.test(val));
if (match) {
applyPlatform(match);
} else if (!val) {
resetPlatform();
}
}
function applyPlatform(p) {
dom.platformInput.value = p.name;
dom.detectedText.textContent = p.name + ' detected';
dom.detectedIcon.className = 'fab ' + p.icon;
dom.urlIcon.innerHTML = `<i class="fab ${p.icon}"></i>`;
dom.urlIcon.style.color = p.color;
// CSS vars for color theming
document.documentElement.style.setProperty('--platform-color', p.color);
document.documentElement.style.setProperty('--platform-rgb', p.rgb);
// Badge colors
dom.platformBadge.style.borderColor = `rgba(${p.rgb}, 0.3)`;
dom.platformBadge.style.background = `rgba(${p.rgb}, 0.1)`;
dom.platformBadge.style.color = p.color;
if (dom.platformPulse) {
dom.platformPulse.style.background = p.color;
dom.platformPulse.style.boxShadow = `0 0 8px ${p.color}`;
}
// Input glow tint
dom.inputWrapper.style.setProperty('--platform-glow', `rgba(${p.rgb}, 0.15)`);
dom.detectionArea.classList.add('visible');
showOptions(p.name);
}
function resetPlatform() {
document.documentElement.style.setProperty('--platform-color', '#8b5cf6');
dom.urlIcon.innerHTML = '<i class="fas fa-link"></i>';
dom.urlIcon.style.color = '';
dom.detectionArea.classList.remove('visible');
}
function showOptions(name) {
dom.ytOptions.classList.add('d-none'); dom.ytOptions.classList.add('d-none');
dom.codecSection.classList.add('d-none'); dom.codecSection.classList.add('d-none');
if (name === 'YouTube') {
if (platform === 'YouTube') {
dom.ytOptions.classList.remove('d-none'); dom.ytOptions.classList.remove('d-none');
updateYtOptions(); updateQualitySelect();
} else if (['TikTok', 'Instagram', 'Twitter'].includes(platform)) { } else if (['TikTok','Instagram','Twitter'].includes(name)) {
dom.codecSection.classList.remove('d-none'); dom.codecSection.classList.remove('d-none');
} }
} }
function updateYtOptions() { function updateQualitySelect() {
const format = document.querySelector('input[name="yt_format"]:checked').value; const fmt = document.querySelector('input[name="yt_format"]:checked')?.value;
if (format === 'mp3') { dom.mp3Select.classList.toggle('d-none', fmt !== 'mp3');
dom.mp3Select.classList.remove('d-none'); dom.mp4Select.classList.toggle('d-none', fmt === 'mp3');
dom.mp4Select.classList.add('d-none');
} else {
dom.mp3Select.classList.add('d-none');
dom.mp4Select.classList.remove('d-none');
}
} }
// --- Processing --- // ---- Submit ----
async function handleSubmit(e) { async function onSubmit(e) {
e.preventDefault(); e.preventDefault();
// UI Switch
dom.formContainer.classList.add('d-none'); dom.formContainer.classList.add('d-none');
dom.processView.classList.remove('d-none'); dom.processView.classList.remove('d-none');
dom.resultView.classList.add('d-none'); dom.resultView.classList.add('d-none');
dom.errorMsg.classList.add('d-none'); dom.errorMsg.classList.add('d-none');
dom.logContent.innerHTML = '';
lastLogIndex = 0;
realLogsActive = false;
setProgress(0);
dom.statusMsg.textContent = 'INITIALIZING...';
startPseudoLogs(); startPseudoLogs();
const formData = new FormData(dom.form);
try { try {
const res = await fetch('/start_download', { method: 'POST', body: formData }); const res = await fetch('/start_download', { method:'POST', body: new FormData(dom.form) });
const data = await res.json(); const data = await res.json();
if (res.ok && data.job_id) { if (res.ok && data.job_id) {
currentJobId = data.job_id; jobId = data.job_id;
startPolling(); startPolling();
} else { } else {
throw new Error(data.error || "Start failed"); throw new Error(data.error || 'Failed to start job');
} }
} catch (err) { } catch (err) {
showError(err.message); showError(err.message);
} }
} }
// ---- Polling ----
function startPolling() { function startPolling() {
pollingInterval = setInterval(async () => { pollTimer = setInterval(async () => {
if (!jobId) return;
try { try {
if(!currentJobId) return; const res = await fetch(`/status?job_id=${jobId}`);
const res = await fetch(`/status?job_id=${currentJobId}`); if (res.status === 404) { showError('Job not found'); return; }
if (res.status === 404) { showError("Job not found"); return; } const s = await res.json();
const status = await res.json();
// Update UI
dom.progressBar.style.width = (status.progress || 0) + "%";
if(status.message) dom.statusMsg.textContent = status.message.toUpperCase();
if (status.status === 'completed') { setProgress(s.progress || 0);
finishJob(status.result_url); if (s.message) dom.statusMsg.textContent = s.message.toUpperCase();
} else if (status.status === 'error' || status.error) {
showError(status.error || status.message); // Show real server logs as they arrive
if (Array.isArray(s.logs) && s.logs.length > lastLogIndex) {
if (!realLogsActive) {
realLogsActive = true;
clearInterval(pseudoTimer);
removeCursor();
}
s.logs.slice(lastLogIndex).forEach(line => addLine(line, 'is-real'));
lastLogIndex = s.logs.length;
} }
} catch (e) {
console.error(e); if (s.status === 'completed') {
onJobDone(s.result_url);
} else if (s.status === 'error' || s.error) {
showError(s.error || s.message);
}
} catch {
// network blip — keep polling
} }
}, 1000); }, 1000);
} }
function finishJob(url) { // ---- Terminal ----
clearInterval(pollingInterval); function addLine(text, cls = '') {
clearInterval(pseudoLogInterval); removeCursor();
dom.processView.classList.add('d-none'); const el = document.createElement('div');
dom.resultView.classList.remove('d-none'); el.className = 'terminal-line' + (cls ? ' ' + cls : '');
dom.resultUrl.href = url; el.textContent = (cls === 'is-real' ? '» ' : '> ') + text;
saveHistory(url); const cur = document.createElement('span');
cur.className = 'term-cursor';
el.appendChild(cur);
dom.logContent.appendChild(el);
dom.logContent.scrollTop = dom.logContent.scrollHeight;
} }
function removeCursor() {
dom.logContent.querySelectorAll('.term-cursor').forEach(c => c.remove());
}
function startPseudoLogs() {
let idx = 0;
addLine(PSEUDO_LOGS[idx++]);
pseudoTimer = setInterval(() => {
if (realLogsActive) { clearInterval(pseudoTimer); return; }
if (idx < PSEUDO_LOGS.length) addLine(PSEUDO_LOGS[idx++]);
}, 2400);
}
// ---- Progress ----
function setProgress(val) {
const pct = Math.min(100, Math.max(0, Math.round(val)));
dom.progressBar.style.width = pct + '%';
if (dom.progressPct) dom.progressPct.textContent = pct + '%';
}
// ---- Finish ----
function onJobDone(url) {
clearInterval(pollTimer);
clearInterval(pseudoTimer);
removeCursor();
const done = document.createElement('div');
done.className = 'terminal-line is-success';
done.textContent = '✓ Upload complete. Your file is ready.';
dom.logContent.appendChild(done);
dom.logContent.scrollTop = dom.logContent.scrollHeight;
setProgress(100);
setTimeout(() => {
dom.processView.classList.add('d-none');
dom.resultView.classList.remove('d-none');
if (url) dom.resultUrl.href = url;
}, 700);
persistHistory(url);
}
// ---- Error ----
function showError(msg) { function showError(msg) {
clearInterval(pollingInterval); clearInterval(pollTimer);
clearInterval(pseudoLogInterval); clearInterval(pseudoTimer);
dom.processView.classList.add('d-none'); dom.processView.classList.add('d-none');
dom.formContainer.classList.remove('d-none'); // Back to form dom.formContainer.classList.remove('d-none');
dom.errorMsg.textContent = msg; dom.errorMsg.textContent = msg;
dom.errorMsg.classList.remove('d-none'); dom.errorMsg.classList.remove('d-none');
} }
// --- Pseudo Logs (CLI Effect) --- // ---- LocalStorage History ----
function startPseudoLogs() { const LS_KEY = 'mdl_history';
dom.logContent.innerHTML = ''; const TTL = 7 * 24 * 60 * 60 * 1000; // 7 days
let index = 0;
function persistHistory(resultUrl) {
function addLine() { const entry = {
if (index >= pseudoLogs.length) index = 0; // Loop or stop url: resultUrl,
const div = document.createElement('div'); source: dom.urlInput.value.trim(),
div.className = 'terminal-line'; platform: dom.platformInput.value,
div.textContent = "> " + pseudoLogs[index]; title: dom.platformInput.value + ' Download',
dom.logContent.appendChild(div); ts: Date.now(),
};
// Auto scroll let data = readHistory();
const win = document.querySelector('.terminal-window .terminal-body'); data.unshift(entry);
win.scrollTop = win.scrollHeight; if (data.length > 30) data.length = 30;
localStorage.setItem(LS_KEY, JSON.stringify(data));
index++; renderHistory();
}
addLine(); // First one immediate
pseudoLogInterval = setInterval(addLine, 2000);
} }
// --- Local History --- function readHistory() {
function loadHistory() { try {
const raw = localStorage.getItem('mdl_history'); const raw = localStorage.getItem(LS_KEY);
if(!raw) { if (!raw) return [];
dom.historyTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-white-50 py-3">No history available</td></tr>'; const data = JSON.parse(raw).filter(i => Date.now() - i.ts < TTL);
localStorage.setItem(LS_KEY, JSON.stringify(data));
return data;
} catch { return []; }
}
function renderHistory() {
const data = readHistory();
if (!dom.historyTbody) return;
if (!data.length) {
dom.historyTbody.innerHTML =
'<tr><td colspan="5" style="text-align:center;padding:2rem;color:rgba(255,255,255,0.25);font-size:0.82rem">No downloads yet</td></tr>';
return; return;
} }
let data = JSON.parse(raw);
// Clean expired (>7 days)
const now = Date.now();
data = data.filter(i => (now - i.ts) < (7 * 24 * 60 * 60 * 1000));
localStorage.setItem('mdl_history', JSON.stringify(data));
renderHistory(data);
}
function renderHistory(data) { dom.historyTbody.innerHTML = '';
dom.historyTableBody.innerHTML = ''; data.forEach((item, i) => {
data.forEach(item => { const d = new Date(item.ts);
const date = new Date(item.ts); const date = d.toLocaleDateString([], { month:'short', day:'numeric' });
const time = date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); const time = d.toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' });
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.className = 'history-row-enter';
tr.style.animationDelay = (i * 0.04) + 's';
tr.innerHTML = ` tr.innerHTML = `
<td class="ps-4 text-white-50 small font-monospace">${time}</td> <td class="ps-4" style="font-family:var(--mono);font-size:0.72rem;color:rgba(255,255,255,0.35);white-space:nowrap">${date} ${time}</td>
<td class="text-center text-primary-light small">${item.platform}</td> <td><span style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.2);color:var(--accent);padding:2px 10px;border-radius:100px;font-size:0.7rem">${escHtml(item.platform)}</span></td>
<td class="text-truncate" style="max-width: 150px;">${item.title || 'Unknown'}</td> <td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(item.title || 'Download')}</td>
<td class="text-center"> <td style="text-align:center">
<a href="${item.source}" target="_blank" class="text-white-50" title="Open source: ${item.source}"> ${item.source
<i class="fas fa-link"></i> ? `<a href="${escHtml(item.source)}" target="_blank" rel="noopener" title="${escHtml(item.source)}"><i class="fas fa-arrow-up-right-from-square fa-sm"></i></a>`
</a> : '<span style="color:rgba(255,255,255,0.2)">—</span>'}
</td> </td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<a href="${item.url}" target="_blank" class="text-primary-light" title="Download"> <a href="${escHtml(item.url)}" target="_blank" rel="noopener"
<i class="fas fa-download"></i> style="background:rgba(139,92,246,0.12);border:1px solid rgba(139,92,246,0.2);color:var(--accent);padding:3px 12px;border-radius:100px;font-size:0.72rem;text-decoration:none;white-space:nowrap">
<i class="fas fa-download me-1"></i>Get
</a> </a>
</td> </td>`;
`; dom.historyTbody.appendChild(tr);
dom.historyTableBody.appendChild(tr);
}); });
} }
function saveHistory(resultUrl) { function clearHistory() {
const pf = dom.platformInput.value; if (!confirm('Clear all download history?')) return;
const sourceUrl = dom.urlInput.value; // Capture source URL localStorage.removeItem(LS_KEY);
renderHistory();
const entry = {
url: resultUrl,
source: sourceUrl,
platform: pf,
title: pf + " Download",
ts: Date.now()
};
let data = JSON.parse(localStorage.getItem('mdl_history') || '[]');
data.unshift(entry);
if(data.length > 20) data.pop(); // Max 20 entries
localStorage.setItem('mdl_history', JSON.stringify(data));
loadHistory();
} }
function clearHistory() { function escHtml(s) {
if(confirm("Really clear history?")) { return String(s)
localStorage.removeItem('mdl_history'); .replace(/&/g,'&amp;').replace(/</g,'&lt;')
loadHistory(); .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
} }
init(); init();
}); });
+707 -198
View File
File diff suppressed because it is too large Load Diff
+133 -116
View File
@@ -3,158 +3,172 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>unknownMedien.dl</title> <title>unknownMedien.dl</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
<!-- Google Fonts --> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head> </head>
<body> <body>
<!-- Animated Background --> <!-- Background layers -->
<div class="bg-gradient-animate"></div> <div class="bg-base"></div>
<div class="bg-mesh"></div>
<div class="bg-glow bg-glow-1"></div> <div class="bg-glow bg-glow-1"></div>
<div class="bg-glow bg-glow-2"></div> <div class="bg-glow bg-glow-2"></div>
<div class="bg-glow bg-glow-3"></div>
<div class="bg-noise"></div>
<!-- Header / Nav --> <!-- Floating particles (CSS-animated) -->
<nav class="navbar fixed-top w-100 p-4 d-flex justify-content-between align-items-center z-3"> <div class="particles" aria-hidden="true">
<div class="brand fw-bold text-white fs-4 tracking-wide"> <span class="particle p1"></span>
unknownMedien<span class="text-primary-light">.dl</span> <span class="particle p2"></span>
<span class="particle p3"></span>
<span class="particle p4"></span>
<span class="particle p5"></span>
<span class="particle p6"></span>
</div>
<!-- Navbar -->
<nav class="navbar fixed-top w-100 px-4 py-3 d-flex justify-content-between align-items-center">
<div class="brand">
unknown<span class="brand-accent">Medien</span><span class="brand-dot">.dl</span>
</div> </div>
<button class="btn btn-glass-icon" data-bs-toggle="modal" data-bs-target="#historyModal" title="History"> <button class="btn btn-glass-icon" data-bs-toggle="modal" data-bs-target="#historyModal" title="History">
<i class="fas fa-history"></i> <i class="fas fa-clock-rotate-left"></i>
</button> </button>
</nav> </nav>
<!-- Main Content (Hero) --> <!-- Main -->
<main class="container-fluid min-vh-100 d-flex flex-column justify-content-center align-items-center position-relative z-2"> <main class="page-center">
<div class="hero-wrapper">
<div class="hero-wrapper w-100 text-center">
<!-- Eyebrow -->
<!-- Headline --> <div class="eyebrow fade-in">
<h1 class="hero-title mb-3 fade-in">Media Downloader</h1> <span class="eyebrow-dot"></span>
<p class="hero-subtitle text-white-50 mb-5 fade-in delay-1"> <span>Media Downloader</span>
Simply paste your link. We handle the rest. </div>
<!-- Title -->
<h1 class="hero-title fade-in delay-1">
Download<br>anything.
</h1>
<p class="hero-sub fade-in delay-2">
Paste your link — we'll handle the rest.
</p> </p>
<!-- Form Area --> <!-- Form -->
<div class="form-container fade-in delay-2"> <div class="form-container fade-in delay-3">
<form id="upload-form"> <form id="upload-form" autocomplete="off">
<input type="hidden" name="platform" id="input-platform" value="SoundCloud"> <input type="hidden" name="platform" id="input-platform" value="SoundCloud">
<!-- Main Input --> <!-- URL Input Row -->
<div class="glass-input-wrapper d-flex align-items-center shadow-lg"> <div class="input-row">
<div class="icon-zone ps-4 text-white-50"> <div class="glass-input-wrapper" id="input-wrapper">
<i class="fas fa-link" id="url-icon"></i> <span class="input-icon" id="url-icon"><i class="fas fa-link"></i></span>
<input type="url" class="url-field" id="url" name="url" required
placeholder="https://soundcloud.com/..." />
<button class="start-btn" type="submit" id="submit-button">
<span class="start-label">Start</span>
<i class="fas fa-arrow-right start-arrow"></i>
<span class="btn-ripple"></span>
</button>
</div> </div>
<input type="url" class="form-control bg-transparent border-0 text-white p-4 fs-5"
id="url" name="url" required placeholder="https://..." autocomplete="off">
<button class="btn btn-action p-4 fw-bold text-uppercase" type="submit" id="submit-button">
Start <i class="fas fa-arrow-right ms-2"></i>
</button>
</div> </div>
<!-- Detected Platform & Options Slide-Down --> <!-- Platform detection -->
<div id="detection-area" class="mt-4 transition-all" style="opacity: 0; transform: translateY(-10px);"> <div id="detection-area" class="detection-area">
<div class="platform-badge" id="platform-badge">
<!-- Badge --> <span class="platform-pulse" id="platform-pulse"></span>
<span class="badge glass-badge px-3 py-2 rounded-pill mb-3"> <i id="detected-icon" class="fab fa-soundcloud"></i>
<i id="detected-icon" class="fas fa-check me-2"></i>
<span id="detected-text">SoundCloud detected</span> <span id="detected-text">SoundCloud detected</span>
</span> </div>
<!-- Dynamic Options --> <!-- Options drawer -->
<div id="options-container" class="options-drawer mx-auto text-start text-white"> <div id="options-container" class="options-container">
<!-- YouTube -->
<!-- YouTube Options --> <div id="youtube-options" class="d-none option-block">
<div id="youtube-options" class="d-none option-group"> <div class="d-flex gap-4 justify-content-center flex-wrap">
<div class="row g-4 justify-content-center"> <div class="option-col">
<div class="col-auto"> <div class="option-label">Format</div>
<label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Format</label>
<div class="switch-toggle"> <div class="switch-toggle">
<input type="radio" name="yt_format" id="format-mp3" value="mp3" checked> <input type="radio" name="yt_format" id="format-mp3" value="mp3" checked>
<label for="format-mp3">MP3</label> <label for="format-mp3">MP3</label>
<input type="radio" name="yt_format" id="format-mp4" value="mp4"> <input type="radio" name="yt_format" id="format-mp4" value="mp4">
<label for="format-mp4">MP4</label> <label for="format-mp4">MP4</label>
<div class="toggle-bg"></div> <div class="toggle-pill"></div>
</div> </div>
</div> </div>
<div class="col-auto" id="quality-wrapper"> <div class="option-col" id="quality-wrapper">
<label class="text-white-50 small text-uppercase fw-bold mb-2 d-block">Quality</label> <div class="option-label">Quality</div>
<select class="form-select glass-select" id="mp3_bitrate" name="mp3_bitrate"> <select class="glass-select" id="mp3_bitrate" name="mp3_bitrate">
<option>Best</option><option selected>192k</option><option>128k</option> <option>Best</option>
<option selected>192k</option>
<option>128k</option>
<option>64k</option>
</select> </select>
<select class="form-select glass-select d-none" id="mp4_quality" name="mp4_quality"> <select class="glass-select d-none" id="mp4_quality" name="mp4_quality">
<option selected>Best</option><option>Medium</option><option>Low</option> <option selected>Best</option>
<option>Medium (~720p)</option>
<option>Low (~480p)</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<!-- Codec -->
<!-- Codec Options --> <div id="codec-options-section" class="d-none option-block text-center">
<div id="codec-options-section" class="d-none option-group mt-3 text-center"> <div class="codec-row">
<div class="d-inline-flex align-items-center glass-panel px-3 py-2 rounded"> <span class="option-label me-3">H.264 Compatibility</span>
<span class="text-white-50 small me-3">Compatibility (H.264)</span> <label class="toggle-switch">
<div class="form-check form-switch m-0"> <input class="ts-input" type="checkbox" role="switch" id="codec-switch">
<input class="form-check-input custom-switch" type="checkbox" role="switch" id="codec-switch"> <span class="ts-track"><span class="ts-thumb"></span></span>
<input type="hidden" name="codec_preference" id="codec_preference" value="original"> <input type="hidden" name="codec_preference" id="codec_preference" value="original">
</div> </label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Error Alert --> <!-- Error -->
<div id="error-message" class="alert glass-alert mt-4 d-none text-danger fw-bold" role="alert"></div> <div id="error-message" class="error-alert d-none" role="alert"></div>
</form> </form>
</div> </div>
<!-- Processing / Terminal View (Hidden initially) --> <!-- Processing view -->
<div id="process-view" class="d-none w-100 max-w-lg mx-auto mt-5"> <div id="process-view" class="d-none process-view">
<div class="process-header">
<!-- Main Status --> <h3 id="status-message" class="status-msg">INITIALIZING...</h3>
<h3 id="status-message" class="text-white fw-bold mb-3 tracking-wide">INITIALIZING...</h3> <span id="progress-pct" class="progress-pct">0%</span>
<!-- Progress Bar -->
<div class="progress glass-progress mb-4">
<div id="progress-bar" class="progress-bar bg-primary-gradient" style="width: 0%"></div>
</div> </div>
<div class="progress-track">
<!-- CLI Log Window --> <div id="progress-bar" class="progress-fill" style="width:0%"></div>
<div class="terminal-window glass-panel text-start p-3 font-monospace small"> </div>
<div class="terminal-header d-flex gap-2 mb-2 opacity-50"> <div class="terminal-window">
<div class="dot bg-danger"></div> <div class="terminal-bar">
<div class="dot bg-warning"></div> <span class="tdot tdot-r"></span>
<div class="dot bg-success"></div> <span class="tdot tdot-y"></span>
<span class="ms-2">worker@unknown-dl:~# process_task</span> <span class="tdot tdot-g"></span>
</div> <span class="tbar-label">worker@unknown-dl ~ process</span>
<div id="pseudo-log-content" class="terminal-body text-primary-light">
<!-- Logs typing here -->
</div> </div>
<div id="pseudo-log-content" class="terminal-body"></div>
</div> </div>
</div> </div>
<!-- Result View --> <!-- Result view -->
<div id="result-view" class="d-none mt-5"> <div id="result-view" class="d-none result-view">
<div class="result-card glass-panel p-5 d-inline-block rounded-4"> <div class="result-card">
<div class="icon-circle bg-success-soft mb-3 mx-auto"> <div class="result-icon-wrap success-ripple">
<i class="fas fa-check text-success fs-2"></i> <div class="result-icon">
<i class="fas fa-check"></i>
</div>
</div> </div>
<h2 class="text-white fw-bold mb-1">Done!</h2> <h2 class="result-title">Done!</h2>
<p class="text-white-50 mb-4">Your file is ready.</p> <p class="result-sub">Your file is ready to download.</p>
<a id="result-url" href="#" target="_blank" class="btn-download">
<a id="result-url" href="#" target="_blank" class="btn btn-primary-glow btn-lg px-5 rounded-pill fw-bold"> <i class="fas fa-download me-2"></i>Download
<i class="fas fa-download me-2"></i> Download
</a> </a>
<div class="result-retry">
<div class="mt-4"> <button class="btn-retry" onclick="location.reload()">
<button class="btn btn-link text-white-50 text-decoration-none btn-sm" onclick="location.reload()"> <i class="fas fa-rotate-left me-1"></i>Convert another
<i class="fas fa-redo me-1"></i> Convert another
</button> </button>
</div> </div>
</div> </div>
@@ -166,32 +180,35 @@
<!-- History Modal --> <!-- History Modal -->
<div class="modal fade" id="historyModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="historyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content glass-modal border-0"> <div class="modal-content glass-modal">
<div class="modal-header border-bottom-white-10"> <div class="modal-header">
<h5 class="modal-title text-white fw-bold">History (Local)</h5> <div>
<h5 class="modal-title">History</h5>
<p class="modal-hint">Stored locally in your browser · 7 days</p>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body p-0"> <div class="modal-body p-0">
<div class="table-responsive" style="max-height: 50vh;"> <div class="table-responsive" style="max-height: 55vh;">
<table class="table table-dark table-hover mb-0 bg-transparent align-middle" id="history-table"> <table class="history-table" id="history-table">
<thead> <thead>
<tr class="text-white-50 small text-uppercase"> <tr>
<th class="ps-4">Time</th> <th class="ps-4">Time</th>
<th class="text-center">Type</th> <th>Platform</th>
<th>Title</th> <th>Title</th>
<th class="text-center">Source</th> <th class="text-center">Source</th>
<th class="text-end pe-4">Download</th> <th class="text-end pe-4">File</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody></tbody>
<!-- JS Fills this -->
</tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="modal-footer border-top-white-10 justify-content-between"> <div class="modal-footer">
<button id="clear-history-button" class="btn btn-outline-danger btn-sm rounded-pill">Clear all</button> <button id="clear-history-button" class="btn-clear-history">
<small class="text-white-50">Saved in browser for 7 days</small> <i class="fas fa-trash-can me-1"></i>Clear all
</button>
<span class="modal-footer-hint">Your data never leaves this browser</span>
</div> </div>
</div> </div>
</div> </div>
@@ -200,4 +217,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script> <script src="{{ url_for('static', filename='script.js') }}"></script>
</body> </body>
</html> </html>