From 5531d5eefd36c10f1da0ea5832bd7539364cc7c0 Mon Sep 17 00:00:00 2001 From: "cool.gitter.choco" Date: Sun, 26 Jan 2025 17:27:08 -0600 Subject: [PATCH] Improved frontend --- .gitignore | 1 - app.py | 2 + package-lock.json | 6 - routes/prgs.py | 24 ++ routes/utils/album.py | 2 +- routes/utils/playlist.py | 2 +- static/css/style.css | 723 +++++++++++++++++++++++++++++++++++++++ static/js/app.js | 397 +++++++++++++++++++-- templates/index.html | 40 ++- 9 files changed, 1156 insertions(+), 41 deletions(-) delete mode 100644 package-lock.json create mode 100644 routes/prgs.py diff --git a/.gitignore b/.gitignore index b209a73..add3bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ /flask_server.log routes/__pycache__/ routes/utils/__pycache__/ - test.sh diff --git a/app.py b/app.py index 13e1d28..9ab27f6 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ from routes.credentials import credentials_bp from routes.album import album_bp from routes.track import track_bp from routes.playlist import playlist_bp +from routes.prgs import prgs_bp import logging import time from pathlib import Path @@ -34,6 +35,7 @@ def create_app(): app.register_blueprint(album_bp, url_prefix='/api/album') app.register_blueprint(track_bp, url_prefix='/api/track') app.register_blueprint(playlist_bp, url_prefix='/api/playlist') + app.register_blueprint(prgs_bp, url_prefix='/api/prgs') # Serve frontend @app.route('/') diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ada89e9..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "spotizer", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/routes/prgs.py b/routes/prgs.py new file mode 100644 index 0000000..c7bfa11 --- /dev/null +++ b/routes/prgs.py @@ -0,0 +1,24 @@ +from flask import Blueprint, send_from_directory, abort +import os + +prgs_bp = Blueprint('prgs', __name__, url_prefix='/api/prgs') + +# Base directory for .prg files +PRGS_DIR = os.path.join(os.getcwd(), 'prgs') + +@prgs_bp.route('/', methods=['GET']) +def get_prg_file(filename): + """ + Serve a .prg file from the prgs directory. + """ + try: + # Security check to prevent path traversal attacks + if not filename.endswith('.prg') or '..' in filename or '/' in filename: + abort(400, "Invalid file request") + + # Ensure the file exists in the directory + return send_from_directory(PRGS_DIR, filename) + except FileNotFoundError: + abort(404, "File not found") + except Exception as e: + abort(500, f"An error occurred: {e}") diff --git a/routes/utils/album.py b/routes/utils/album.py index 83653c3..f0f2aaa 100644 --- a/routes/utils/album.py +++ b/routes/utils/album.py @@ -29,7 +29,7 @@ def download_album(service, url, main, fallback=None): recursive_quality=True, recursive_download=False, not_interface=False, - make_zip=True, + make_zip=False, method_save=1 ) except Exception as e: diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index bb8c5ed..72321b1 100644 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -29,7 +29,7 @@ def download_playlist(service, url, main, fallback=None): recursive_quality=True, recursive_download=False, not_interface=False, - make_zip=True, + make_zip=False, method_save=1 ) except Exception as e: diff --git a/static/css/style.css b/static/css/style.css index 1f91602..6c3606c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -269,4 +269,727 @@ body { .service-tabs button.active { background: #1DB954; +} +/* Add to style.css */ +.config-bar { + position: fixed; + top: 20px; + right: 20px; + background: #181818; + padding: 15px; + border-radius: 8px; + z-index: 1000; +} + +.config-item { + margin-bottom: 10px; +} + +.config-item label { + display: block; + margin-bottom: 5px; +} + +.switch { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 20px; +} + +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #1DB954; +} + +input:checked + .slider:before { + transform: translateX(20px); +} + +.queue-btn { + background: #1DB954; + color: white; + border: none; + padding: 8px 15px; + border-radius: 20px; + cursor: pointer; +} + +.sidebar.right { + right: -350px; + left: auto; +} + +.sidebar.right.active { + right: 0; +} + +.queue-item { + background: #2a2a2a; + padding: 10px; + margin: 10px 0; + border-radius: 5px; +} + +.queue-item .log { + font-family: monospace; + font-size: 12px; + color: #b3b3b3; + margin-top: 5px; +} +/* Add these styles to your existing CSS */ + +/* Download Queue styles */ +#downloadQueue { + position: fixed; + top: 20px; + right: 20px; + width: 350px; + background: #181818; + border-radius: 8px; + padding: 15px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + transform: translateX(120%); + transition: transform 0.3s ease; + z-index: 1002; +} + +#downloadQueue.active { + transform: translateX(0); +} + +.queue-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.queue-title { + font-size: 18px; + font-weight: 600; +} + +.queue-close { + background: none; + border: none; + color: #b3b3b3; + cursor: pointer; + padding: 5px; +} + +#queueItems { + max-height: 60vh; + overflow-y: auto; +} + +.queue-item { + background: #2a2a2a; + padding: 12px; + margin-bottom: 10px; + border-radius: 6px; + transition: transform 0.2s ease; +} + +.queue-item:hover { + transform: translateX(5px); +} + +.queue-item .title { + font-weight: 500; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.queue-item .type { + font-size: 12px; + color: #1DB954; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +.queue-item .log { + font-size: 13px; + color: #b3b3b3; + line-height: 1.4; + font-family: 'SF Mono', Menlo, monospace; +} + +/* Status message colors */ +.log--success { color: #1DB954 !important; } +.log--error { color: #ff5555 !important; } +.log--warning { color: #ffaa00 !important; } +.log--info { color: #4a90e2 !important; } + +/* Progress animation */ +@keyframes progress-pulse { + 0% { opacity: 0.5; } + 50% { opacity: 1; } + 100% { opacity: 0.5; } +} + +.progress-indicator { + display: inline-block; + margin-left: 8px; + animation: progress-pulse 1.5s infinite; +} + +/* Enhanced loading states */ +.loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 3px solid rgba(255,255,255,0.3); + border-radius: 50%; + border-top-color: #1DB954; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Timeout warning */ +.timeout-warning { + position: relative; + padding-left: 24px; +} + +.timeout-warning::before { + content: "⚠️"; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); +} + +/* Form enhancements */ +#credentialForm { + margin-top: 20px; +} + +#credentialName { + margin-bottom: 15px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .search-header { + flex-direction: column; + } + + .search-input, + .search-type, + .search-button { + width: 100%; + } + + #downloadQueue { + width: 90%; + right: 5%; + top: 70px; + } + + .sidebar { + width: 100%; + left: -100%; + } + + .sidebar.active { + left: 0; + } +} + +/* Status bar animations */ +.status-bar { + height: 3px; + background: #1DB954; + width: 0; + transition: width 0.3s ease; + margin-top: 8px; +} + +/* Error traceback styling */ +.traceback { + background: #2a2a2a; + padding: 10px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: #ff5555; + margin-top: 10px; + max-height: 200px; + overflow-y: auto; +} + +/* Queue item states */ +.queue-item--complete { + border-left: 4px solid #1DB954; +} + +.queue-item--error { + border-left: 4px solid #ff5555; +} + +.queue-item--processing { + border-left: 4px solid #4a90e2; +} + +/* Progress percentage styling */ +.progress-percent { + float: right; + font-weight: bold; + color: #1DB954; +} + +/* Hover tooltip for long messages */ +.queue-item .log { + position: relative; +} + +/* Download button styling */ +.download-btn { + background-color: #1DB954; + color: white; + border: none; + padding: 8px 15px; + border-radius: 20px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + justify-content: center; +} + +.download-btn:hover { + background-color: #1ed760; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(29, 185, 84, 0.3); +} + +.download-btn:active { + transform: translateY(0); + box-shadow: none; +} + +.download-btn::before { + content: "↓"; + font-weight: bold; + font-size: 16px; +} + +/* Select dropdown styling */ +#spotifyAccountSelect, +#deezerAccountSelect { + background: #2a2a2a; + color: white; + border: 1px solid #404040; + border-radius: 8px; + padding: 10px 15px; + font-size: 14px; + width: 100%; + margin: 5px 0 15px; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px; + transition: border-color 0.3s ease; +} + +#spotifyAccountSelect:focus, +#deezerAccountSelect:focus { + outline: none; + border-color: #1DB954; + box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.2); +} + +/* Style the dropdown options */ +#spotifyAccountSelect option, +#deezerAccountSelect option { + background: #181818; + color: white; + padding: 10px; +} + +/* Hover state for options (limited browser support) */ +#spotifyAccountSelect option:hover, +#deezerAccountSelect option:hover { + background: #1DB954; +} + +/* Disabled state styling */ +#spotifyAccountSelect:disabled, +#deezerAccountSelect:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + #spotifyAccountSelect, + #deezerAccountSelect { + font-size: 16px; + padding: 12px 20px; + } + + .download-btn { + padding: 10px 20px; + font-size: 16px; + } +} + +/* Add queue icon styling */ +.queue-icon { + position: fixed; + top: 20px; + right: 70px; /* Adjust based on settings icon position */ + background: none; + border: none; + color: white; + font-size: 1.5em; + cursor: pointer; + z-index: 1000; + } + + .queue-icon:hover { + color: #1DB954; + } + + /* Keep the existing queue sidebar styles */ + #downloadQueue { + /* existing styles */ + right: -350px; + transition: right 0.3s; + } + + #downloadQueue.active { + right: 0; + } + + /* Mobile-First Enhancements */ +@media screen and (max-width: 768px) { + /* Viewport-friendly base sizing */ + html { + font-size: 14px; + } + + /* Container adjustments */ + .container { + padding: 10px; + max-width: 100%; + } + + /* Stack search elements vertically */ + .search-header { + flex-direction: column; + gap: 10px; + padding: 15px 0; + position: relative; + top: auto; + } + + /* Improve touch targets */ + .search-input, + .search-type, + .search-button { + width: 100%; + padding: 15px 20px; + font-size: 1rem; + } + + /* Adjust grid layout for smaller screens */ + .results-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 15px; + } + + /* Optimize card content spacing */ + .result-card { + padding: 12px; + } + + .track-title { + font-size: 14px; + } + + .track-artist { + font-size: 12px; + } + + /* Mobile-friendly sidebar */ + .sidebar { + width: 100%; + left: -100%; + } + + .sidebar.active { + left: 0; + } + + /* Queue positioning adjustments */ + #downloadQueue { + width: 95%; + right: 2.5%; + top: 60px; + } + + /* Icon positioning */ + .settings-icon { + top: 15px; + left: 15px; + } + + .queue-icon { + top: 15px; + right: 15px; + } + + /* Form element adjustments */ + .form-group input, + .form-group textarea { + padding: 12px; + } +} + +/* Additional Mobile Optimizations */ +@media screen and (max-width: 480px) { + /* Further reduce grid item size */ + .results-grid { + grid-template-columns: 1fr 1fr; + } + + /* Increase body text contrast */ + body { + font-size: 16px; + } + + /* Enhance tap target sizing */ + button, + .result-card { + min-height: 48px; + } + + /* Prevent text overflow */ + .track-title, + .track-artist { + white-space: normal; + overflow: visible; + text-overflow: clip; + } +} + +/* Input element mobile optimization */ +input, +select, +textarea { + font-size: 16px !important; /* Prevent iOS zoom */ +} + +/* Touch interaction improvements */ +button { + touch-action: manipulation; +} + +.result-card { + -webkit-tap-highlight-color: transparent; +} + +/* Prevent layout shift on scrollbar appearance */ +html { + overflow-y: scroll; +} + +/* Modified Button Positioning */ +.settings-icon { + position: static; /* Remove fixed positioning */ + order: -1; /* Move to start of flex container */ + margin-right: 15px; + font-size: 22px; +} + +.queue-icon { + position: static; /* Remove fixed positioning */ + order: 2; /* Place after search button */ + margin-left: 15px; + font-size: 22px; +} + +/* Updated Search Header */ +.search-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 30px; + position: sticky; + top: 0; + background-color: #121212; + padding: 20px 0; + z-index: 100; +} + +/* Mobile Responsiveness */ +@media (max-width: 768px) { + .search-header { + flex-wrap: wrap; + gap: 12px; + padding: 15px 0; + } + + .settings-icon, + .queue-icon { + margin: 0; + order: 0; /* Reset order for mobile */ + font-size: 24px; + } + + .search-input, + .search-type { + order: 1; + width: 100%; + } + + .search-button { + order: 2; + width: 100%; + } + + .queue-icon { + order: 3; + margin-left: auto; + } +} + +/* Existing queue icon styles remain the same */ +.queue-icon:hover { + color: #1DB954; +} + +/* Updated Sidebar Animations */ +.sidebar { + position: fixed; + top: 0; + width: 350px; + height: 100vh; + background: #181818; + padding: 20px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1001; + overflow-y: auto; + box-shadow: 0 0 15px rgba(0,0,0,0.3); +} + +/* Settings Sidebar Specific */ +#settingsSidebar { + left: -350px; +} + +#settingsSidebar.active { + left: 0; + box-shadow: 20px 0 30px rgba(0,0,0,0.4); +} + +/* Download Queue Specific */ +#downloadQueue { + right: -350px; + left: auto; +} + +#downloadQueue.active { + right: 0; + box-shadow: -20px 0 30px rgba(0,0,0,0.4); +} + +/* Enhanced Transition Effects */ +.sidebar { + transition: + left 0.3s cubic-bezier(0.4, 0, 0.2, 1), + right 0.3s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.3s ease; +} + +/* Queue Item Animations */ +.queue-item { + transition: + transform 0.2s ease, + opacity 0.3s ease, + background-color 0.3s ease; + opacity: 1; +} + +.queue-item:not(.active):hover { + transform: translateX(5px); + background-color: #333; +} + +.queue-item.entering { + opacity: 0; + transform: translateX(20px); +} + +.queue-item.exiting { + opacity: 0; + transform: translateX(-20px); +} + +/* Mobile Responsiveness Adjustments */ +@media (max-width: 768px) { + .sidebar { + width: 100%; + box-shadow: none; + } + + #settingsSidebar { + left: -100%; + } + + #downloadQueue { + right: -100%; + } + + .sidebar.active { + box-shadow: 0 0 30px rgba(0,0,0,0.4); + } } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 52f9933..a9027eb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,10 +1,3 @@ -function logRequest(method, url, body = null) { - console.log(`Sending ${method} request to: ${url}`); - if (body) { - console.log('Request payload:', body); - } -} - const serviceConfig = { spotify: { fields: [ @@ -32,8 +25,11 @@ const serviceConfig = { let currentService = 'spotify'; let currentCredential = null; +let downloadQueue = {}; +let prgInterval = null; document.addEventListener('DOMContentLoaded', () => { + const searchButton = document.getElementById('searchButton'); const searchInput = document.getElementById('searchInput'); const settingsIcon = document.getElementById('settingsIcon'); @@ -41,6 +37,9 @@ document.addEventListener('DOMContentLoaded', () => { const closeSidebar = document.getElementById('closeSidebar'); const serviceTabs = document.querySelectorAll('.tab-button'); + // Initialize configuration + initConfig(); + // Search functionality searchButton.addEventListener('click', performSearch); searchInput.addEventListener('keypress', (e) => { @@ -71,7 +70,92 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('credentialForm').addEventListener('submit', handleCredentialSubmit); }); -// Search functions remain the same +async function initConfig() { + loadConfig(); + console.log(loadConfig()) + await updateAccountSelectors(); + + // Event listeners + document.getElementById('fallbackToggle').addEventListener('change', () => { + saveConfig(); + updateAccountSelectors(); + }); + + const accountSelects = ['spotifyAccountSelect', 'deezerAccountSelect']; + accountSelects.forEach(id => { + document.getElementById(id).addEventListener('change', () => { + saveConfig(); + updateAccountSelectors(); + }); + }); +} + +async function updateAccountSelectors() { + try { + // Get current saved configuration + const saved = JSON.parse(localStorage.getItem('activeConfig')) || {}; + + // Fetch available credentials + const [spotifyResponse, deezerResponse] = await Promise.all([ + fetch('/api/credentials/spotify'), + fetch('/api/credentials/deezer') + ]); + + const spotifyAccounts = await spotifyResponse.json(); + const deezerAccounts = await deezerResponse.json(); + + // Update Spotify selector + const spotifySelect = document.getElementById('spotifyAccountSelect'); + const isValidSpotify = spotifyAccounts.includes(saved.spotify); + spotifySelect.innerHTML = spotifyAccounts.map(a => + `` + ).join(''); + + // Validate/correct Spotify selection + if (!isValidSpotify && spotifyAccounts.length > 0) { + spotifySelect.value = spotifyAccounts[0]; + saved.spotify = spotifyAccounts[0]; + localStorage.setItem('activeConfig', JSON.stringify(saved)); + } + + // Update Deezer selector + const deezerSelect = document.getElementById('deezerAccountSelect'); + const isValidDeezer = deezerAccounts.includes(saved.deezer); + deezerSelect.innerHTML = deezerAccounts.map(a => + `` + ).join(''); + + // Validate/correct Deezer selection + if (!isValidDeezer && deezerAccounts.length > 0) { + deezerSelect.value = deezerAccounts[0]; + saved.deezer = deezerAccounts[0]; + localStorage.setItem('activeConfig', JSON.stringify(saved)); + } + + // Handle empty states + [spotifySelect, deezerSelect].forEach((select, index) => { + const accounts = index === 0 ? spotifyAccounts : deezerAccounts; + if (accounts.length === 0) { + select.innerHTML = ''; + select.value = ''; + } + }); + + } catch (error) { + console.error('Error updating account selectors:', error); + } +} + + +function toggleDownloadQueue() { + const queueSidebar = document.getElementById('downloadQueue'); + queueSidebar.classList.toggle('active'); + + // Update button state + const queueIcon = document.getElementById('queueIcon'); + queueIcon.textContent = queueSidebar.classList.contains('active') ? '📭' : '📥'; +} + function performSearch() { const query = document.getElementById('searchInput').value.trim(); const searchType = document.getElementById('searchType').value; @@ -90,17 +174,29 @@ function performSearch() { if (data.error) throw new Error(data.error); const items = data.data[`${searchType}s`]?.items; - resultsContainer.innerHTML = items?.length - ? items.map(item => createResultCard(item, searchType)).join('') - : '
No results found
'; + if (!items || !items.length) { + resultsContainer.innerHTML = '
No results found
'; + return; + } + + resultsContainer.innerHTML = items.map(item => createResultCard(item, searchType)).join(''); + + const cards = resultsContainer.querySelectorAll('.result-card'); + cards.forEach((card, index) => { + // Add download handler + card.querySelector('.download-btn').addEventListener('click', async (e) => { + e.stopPropagation(); + const url = e.target.dataset.url; + const type = e.target.dataset.type; + startDownload(url, type, items[index]); + card.remove(); + }); + }); }) .catch(error => showError(error.message)); } function createResultCard(item, type) { - const card = document.createElement('div'); - card.className = 'result-card'; - let imageUrl, title, subtitle, details; switch(type) { @@ -133,17 +229,185 @@ function createResultCard(item, type) { break; } - card.innerHTML = ` - ${type} cover -
${title}
-
${subtitle}
-
${details}
+ return ` +
+ ${type} cover +
${title}
+
${subtitle}
+
${details}
+ +
`; - card.addEventListener('click', () => window.open(item.external_urls.spotify, '_blank')); - return card; } -// Credential management functions +async function startDownload(url, type, item) { + const fallbackEnabled = document.getElementById('fallbackToggle').checked; + const spotifyAccount = document.getElementById('spotifyAccountSelect').value; + const deezerAccount = document.getElementById('deezerAccountSelect').value; + + let apiUrl = `/api/${type}/download?service=spotify&url=${encodeURIComponent(url)}`; + + if (fallbackEnabled) { + apiUrl += `&main=${deezerAccount}&fallback=${spotifyAccount}`; + } else { + apiUrl += `&main=${spotifyAccount}`; + } + + try { + const response = await fetch(apiUrl); + const data = await response.json(); + + addToQueue(item, type, data.prg_file); + startMonitoringQueue(); + } catch (error) { + showError('Download failed: ' + error.message); + } +} + +function addToQueue(item, type, prgFile) { + const queueId = Date.now().toString(); + downloadQueue[queueId] = { + item, + type, + prgFile, + element: createQueueItem(item, type, prgFile), + lastLineCount: 0, + lastUpdated: Date.now(), + hasEnded: false + }; + document.getElementById('queueItems').appendChild(downloadQueue[queueId].element); +} + + +function createQueueItem(item, type, prgFile) { + const div = document.createElement('div'); + div.className = 'queue-item'; + div.innerHTML = ` +
${item.name}
+
${type.charAt(0).toUpperCase() + type.slice(1)}
+
Initializing download...
+ `; + return div; +} + +function startMonitoringQueue() { + if (!prgInterval) { + prgInterval = setInterval(async () => { + const queueEntries = Object.entries(downloadQueue); + if (queueEntries.length === 0) { + clearInterval(prgInterval); + prgInterval = null; + return; + } + + let activeEntries = 0; + + for (const [id, entry] of queueEntries) { + if (entry.hasEnded) continue; + activeEntries++; + + try { + const response = await fetch(`/api/prgs/${entry.prgFile}`); + const log = await response.text(); + const lines = log.split('\n').filter(line => line.trim() !== ''); + const logElement = document.getElementById(`log-${entry.prgFile}`); + + // Process new lines + if (lines.length > entry.lastLineCount) { + const newLines = lines.slice(entry.lastLineCount); + entry.lastLineCount = lines.length; + entry.lastUpdated = Date.now(); + + for (const line of newLines) { + try { + const data = JSON.parse(line); + + // Store status in queue entry + if (data.status === 'error' || data.status === 'complete') { + entry.status = data.status; + } + + // Handle error status with retry button + if (data.status === 'error') { + logElement.innerHTML = ` + ${getStatusMessage(data)} + + + `; + + // Retry handler + logElement.querySelector('.retry-btn').addEventListener('click', (e) => { + e.stopPropagation(); + startDownload(entry.item.external_urls.spotify, entry.type, entry.item); + delete downloadQueue[id]; + entry.element.remove(); + }); + + // Close handler + logElement.querySelector('.close-btn').addEventListener('click', (e) => { + e.stopPropagation(); + delete downloadQueue[id]; + entry.element.remove(); + }); + + entry.element.classList.add('failed'); + entry.hasEnded = true; + } else { + logElement.textContent = getStatusMessage(data); + } + + // Handle terminal statuses + if (data.status === 'error' || data.status === 'complete') { + entry.hasEnded = true; + entry.status = data.status; + if (data.status === 'error' && data.traceback) { + console.error('Server error:', data.traceback); + } + break; + } + } catch (e) { + console.error('Invalid PRG line:', line); + } + } + } + + // Handle timeout + if (Date.now() - entry.lastUpdated > 180000) { + logElement.textContent = 'Download timed out (3 minutes inactivity)'; + entry.hasEnded = true; + entry.status = 'timeout'; + } + + // Cleanup completed entries only + if (entry.hasEnded && entry.status === 'complete') { + setTimeout(() => { + delete downloadQueue[id]; + entry.element.remove(); + }, 5000); + } + + } catch (error) { + console.error('Status check failed:', error); + entry.hasEnded = true; + entry.status = 'error'; + document.getElementById(`log-${entry.prgFile}`).textContent = 'Status check error'; + } + } + + // Stop interval if no active entries + if (activeEntries === 0) { + clearInterval(prgInterval); + prgInterval = null; + } + }, 2000); + } +} + + async function loadCredentials(service) { try { const response = await fetch(`/api/credentials/${service}`); @@ -160,7 +424,7 @@ function renderCredentialsList(service, credentials) { ${name}
- +
`).join(''); @@ -168,10 +432,35 @@ function renderCredentialsList(service, credentials) { list.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', async (e) => { try { - await fetch(`/api/credentials/${service}/${e.target.dataset.name}`, { method: 'DELETE' }); + const service = e.target.dataset.service; + const name = e.target.dataset.name; + + if (!service || !name) { + throw new Error('Missing credential information'); + } + + const response = await fetch(`/api/credentials/${service}/${name}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('Failed to delete credential'); + } + + // Update active account if deleted credential was selected + const accountSelect = document.getElementById(`${service}AccountSelect`); + if (accountSelect.value === name) { + accountSelect.value = ''; + saveConfig(); + } + + // Refresh UI loadCredentials(service); + await updateAccountSelectors(); + } catch (error) { showSidebarError(error.message); + console.error('Delete error:', error); } }); }); @@ -182,9 +471,11 @@ function renderCredentialsList(service, credentials) { const name = e.target.dataset.name; try { + // Switch to correct service tab document.querySelector(`[data-service="${service}"]`).click(); await new Promise(resolve => setTimeout(resolve, 50)); + // Load credential data const response = await fetch(`/api/credentials/${service}/${name}`); const data = await response.json(); @@ -227,21 +518,16 @@ async function handleCredentialSubmit(e) { const name = nameInput.value.trim(); try { - // Validate name exists for new credentials if (!currentCredential && !name) { throw new Error('Credential name is required'); } - // Collect form data const formData = {}; serviceConfig[service].fields.forEach(field => { formData[field.id] = document.getElementById(field.id).value.trim(); }); - // Validate using service config const data = serviceConfig[service].validator(formData); - - // Use currentCredential for updates, name for new entries const endpointName = currentCredential || name; const method = currentCredential ? 'PUT' : 'POST'; @@ -256,8 +542,12 @@ async function handleCredentialSubmit(e) { throw new Error(errorData.error || 'Failed to save credentials'); } + // Refresh and persist after credential changes + await updateAccountSelectors(); + saveConfig(); loadCredentials(service); resetForm(); + } catch (error) { showSidebarError(error.message); console.error('Submission error:', error); @@ -289,4 +579,51 @@ function showSidebarError(message) { const errorDiv = document.getElementById('sidebarError'); errorDiv.textContent = message; setTimeout(() => errorDiv.textContent = '', 3000); -} \ No newline at end of file +} + +function getStatusMessage(data) { + switch (data.status) { + case 'downloading': + return `Downloading ${data.song || 'track'} by ${data.artist || 'artist'}...`; + case 'progress': + if (data.type === 'album') { + return `Processing track ${data.current_track}/${data.total_tracks} (${data.percentage.toFixed(1)}%): ${data.song}`; + } else { + return `${data.percentage.toFixed(1)}% complete`; + } + case 'done': + return `Finished: ${data.song} by ${data.artist}`; + case 'initializing': + return `Initializing ${data.type} download for ${data.album || data.artist}...`; + case 'error': + return `Error: ${data.message || 'Unknown error'}`; + case 'complete': + return 'Download completed successfully'; + default: + return data.status; + } +} + +function saveConfig() { + const config = { + spotify: document.getElementById('spotifyAccountSelect').value, + deezer: document.getElementById('deezerAccountSelect').value, + fallback: document.getElementById('fallbackToggle').checked + }; + localStorage.setItem('activeConfig', JSON.stringify(config)); +} + + +function loadConfig() { + const saved = JSON.parse(localStorage.getItem('activeConfig')) || {}; + + // Set values only if they exist in the DOM + const spotifySelect = document.getElementById('spotifyAccountSelect'); + const deezerSelect = document.getElementById('deezerAccountSelect'); + + if (spotifySelect) spotifySelect.value = saved.spotify || ''; + if (deezerSelect) deezerSelect.value = saved.deezer || ''; + + const fallbackToggle = document.getElementById('fallbackToggle'); + if (fallbackToggle) fallbackToggle.checked = !!saved.fallback; +} diff --git a/templates/index.html b/templates/index.html index 390b8b7..06788fd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,17 +7,41 @@ - +
+ +
+ + + \ No newline at end of file