diff --git a/routes/album.py b/routes/album.py index ae49de5..0b6e86a 100755 --- a/routes/album.py +++ b/routes/album.py @@ -27,7 +27,7 @@ class FlushingFileWrapper: def flush(self): self.file.flush() -def download_task(service, url, main, fallback, prg_path): +def download_task(service, url, main, fallback, quality, fall_quality, prg_path): try: from routes.utils.album import download_album with open(prg_path, 'w') as f: @@ -40,7 +40,9 @@ def download_task(service, url, main, fallback, prg_path): service=service, url=url, main=main, - fallback=fallback + fallback=fallback, + quality=quality, + fall_quality=fall_quality ) flushing_file.write(json.dumps({"status": "complete"}) + "\n") except Exception as e: @@ -67,6 +69,14 @@ def handle_download(): url = request.args.get('url') main = request.args.get('main') fallback = request.args.get('fallback') + quality = request.args.get('quality') + fall_quality = request.args.get('fall_quality') + + # Sanitize main and fallback to prevent directory traversal + if main: + main = os.path.basename(main) + if fallback: + fallback = os.path.basename(fallback) if not all([service, url, main]): return Response( @@ -75,6 +85,56 @@ def handle_download(): mimetype='application/json' ) + # Validate credentials based on service and fallback + try: + if service == 'spotify': + if fallback: + # Validate Deezer main and Spotify fallback credentials + deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json')) + if not os.path.isfile(deezer_creds_path): + return Response( + json.dumps({"error": "Invalid Deezer credentials directory"}), + status=400, + mimetype='application/json' + ) + spotify_fallback_path = os.path.abspath(os.path.join('./creds/spotify', fallback, 'credentials.json')) + if not os.path.isfile(spotify_fallback_path): + return Response( + json.dumps({"error": "Invalid Spotify fallback credentials directory"}), + status=400, + mimetype='application/json' + ) + else: + # Validate Spotify main credentials + spotify_creds_path = os.path.abspath(os.path.join('./creds/spotify', main, 'credentials.json')) + if not os.path.isfile(spotify_creds_path): + return Response( + json.dumps({"error": "Invalid Spotify credentials directory"}), + status=400, + mimetype='application/json' + ) + elif service == 'deezer': + # Validate Deezer main credentials + deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json')) + if not os.path.isfile(deezer_creds_path): + return Response( + json.dumps({"error": "Invalid Deezer credentials directory"}), + status=400, + mimetype='application/json' + ) + else: + return Response( + json.dumps({"error": "Unsupported service"}), + status=400, + mimetype='application/json' + ) + except Exception as e: + return Response( + json.dumps({"error": f"Credential validation failed: {str(e)}"}), + status=500, + mimetype='application/json' + ) + filename = generate_random_filename() prg_dir = './prgs' os.makedirs(prg_dir, exist_ok=True) @@ -82,7 +142,7 @@ def handle_download(): Process( target=download_task, - args=(service, url, main, fallback, prg_path) + args=(service, url, main, fallback, quality, fall_quality, prg_path) ).start() return Response( diff --git a/routes/playlist.py b/routes/playlist.py index bf288c5..21e9456 100755 --- a/routes/playlist.py +++ b/routes/playlist.py @@ -26,7 +26,7 @@ class FlushingFileWrapper: def flush(self): self.file.flush() -def download_task(service, url, main, fallback, prg_path): +def download_task(service, url, main, fallback, quality, fall_quality, prg_path): try: from routes.utils.playlist import download_playlist with open(prg_path, 'w') as f: @@ -39,7 +39,9 @@ def download_task(service, url, main, fallback, prg_path): service=service, url=url, main=main, - fallback=fallback + fallback=fallback, + quality=quality, + fall_quality=fall_quality ) flushing_file.write(json.dumps({"status": "complete"}) + "\n") except Exception as e: @@ -66,6 +68,8 @@ def handle_download(): url = request.args.get('url') main = request.args.get('main') fallback = request.args.get('fallback') + quality = request.args.get('quality') + fall_quality = request.args.get('fall_quality') if not all([service, url, main]): return Response( @@ -81,7 +85,7 @@ def handle_download(): Process( target=download_task, - args=(service, url, main, fallback, prg_path) + args=(service, url, main, fallback, quality, fall_quality, prg_path) ).start() return Response( diff --git a/routes/track.py b/routes/track.py index de2da7d..1f116bc 100755 --- a/routes/track.py +++ b/routes/track.py @@ -26,7 +26,7 @@ class FlushingFileWrapper: def flush(self): self.file.flush() -def download_task(service, url, main, fallback, prg_path): +def download_task(service, url, main, fallback, quality, fall_quality, prg_path): try: from routes.utils.track import download_track with open(prg_path, 'w') as f: @@ -39,7 +39,9 @@ def download_task(service, url, main, fallback, prg_path): service=service, url=url, main=main, - fallback=fallback + fallback=fallback, + quality=quality, + fall_quality=fall_quality ) flushing_file.write(json.dumps({"status": "complete"}) + "\n") except Exception as e: @@ -66,6 +68,8 @@ def handle_download(): url = request.args.get('url') main = request.args.get('main') fallback = request.args.get('fallback') + quality = request.args.get('quality') + fall_quality = request.args.get('fall_quality') if not all([service, url, main]): return Response( @@ -81,7 +85,7 @@ def handle_download(): Process( target=download_task, - args=(service, url, main, fallback, prg_path) + args=(service, url, main, fallback, quality, fall_quality, prg_path) ).start() return Response( diff --git a/routes/utils/album.py b/routes/utils/album.py index 0565ff4..ca9e4a7 100755 --- a/routes/utils/album.py +++ b/routes/utils/album.py @@ -4,10 +4,15 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_album(service, url, main, fallback=None): +def download_album(service, url, main, fallback=None, quality=None, fall_quality=None): try: if service == 'spotify': + if fallback: + if quality is None: + quality = 'FLAC' + if fall_quality is None: + fall_quality='HIGH' # First attempt: use DeeLogin's download_albumspo with the 'main' (Deezer credentials) try: # Load Deezer credentials from 'main' under deezer directory @@ -23,7 +28,7 @@ def download_album(service, url, main, fallback=None): dl.download_albumspo( link_album=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -39,7 +44,7 @@ def download_album(service, url, main, fallback=None): spo.download_album( link_album=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=fall_quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -54,13 +59,15 @@ def download_album(service, url, main, fallback=None): ) from e2 else: # Original behavior: use Spotify main + if quality is None: + quality ='HIGH' creds_dir = os.path.join('./creds/spotify', main) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) spo = SpoLogin(credentials_path=credentials_path) spo.download_album( link_album=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -68,6 +75,8 @@ def download_album(service, url, main, fallback=None): make_zip=False ) elif service == 'deezer': + if quality is None: + quality='FLAC' # Existing code remains the same, ignoring fallback creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) @@ -79,7 +88,7 @@ def download_album(service, url, main, fallback=None): dl.download_albumdee( link_album=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=True, recursive_download=False, method_save=1, diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index 9a49ffb..91a8809 100755 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -4,10 +4,15 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_playlist(service, url, main, fallback=None): +def download_playlist(service, url, main, fallback=None, quality=None, fall_quality=None): try: + if service == 'spotify': if fallback: + if quality is None: + quality = 'FLAC' + if fall_quality is None: + fall_quality='HIGH' # First attempt: use DeeLogin's download_playlistspo with the 'main' (Deezer credentials) try: # Load Deezer credentials from 'main' under deezer directory @@ -23,7 +28,7 @@ def download_playlist(service, url, main, fallback=None): dl.download_playlistspo( link_playlist=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -39,7 +44,7 @@ def download_playlist(service, url, main, fallback=None): spo.download_playlist( link_playlist=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=fall_quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -54,13 +59,15 @@ def download_playlist(service, url, main, fallback=None): ) from e2 else: # Original behavior: use Spotify main + if quality is None: + quality='HIGH' creds_dir = os.path.join('./creds/spotify', main) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) spo = SpoLogin(credentials_path=credentials_path) spo.download_playlist( link_playlist=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=quality, recursive_quality=True, recursive_download=False, not_interface=False, @@ -68,6 +75,8 @@ def download_playlist(service, url, main, fallback=None): make_zip=False ) elif service == 'deezer': + if quality is None: + quality='FLAC' # Existing code for Deezer, using main as Deezer account creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) @@ -79,7 +88,7 @@ def download_playlist(service, url, main, fallback=None): dl.download_playlistdee( link_playlist=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=False, recursive_download=False, method_save=1, diff --git a/routes/utils/track.py b/routes/utils/track.py index bb23aaa..96ae7bd 100755 --- a/routes/utils/track.py +++ b/routes/utils/track.py @@ -4,10 +4,15 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_track(service, url, main, fallback=None): +def download_track(service, url, main, fallback=None, quality=None, fall_quality=None): try: + if service == 'spotify': if fallback: + if quality is None: + quality = 'FLAC' + if fall_quality is None: + fall_quality='HIGH' # First attempt: use Deezer's download_trackspo with 'main' (Deezer credentials) try: deezer_creds_dir = os.path.join('./creds/deezer', main) @@ -20,7 +25,7 @@ def download_track(service, url, main, fallback=None): dl.download_trackspo( link_track=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=False, recursive_download=False, not_interface=False, @@ -33,7 +38,7 @@ def download_track(service, url, main, fallback=None): spo.download_track( link_track=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=fall_quality, recursive_quality=False, recursive_download=False, not_interface=False, @@ -41,19 +46,23 @@ def download_track(service, url, main, fallback=None): ) else: # Directly use Spotify main account + if quality is None: + quality='HIGH' creds_dir = os.path.join('./creds/spotify', main) credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) spo = SpoLogin(credentials_path=credentials_path) spo.download_track( link_track=url, output_dir="./downloads", - quality_download="HIGH", + quality_download=quality, recursive_quality=False, recursive_download=False, not_interface=False, method_save=1 ) elif service == 'deezer': + if quality is None: + quality='FLAC' # Deezer download logic remains unchanged creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) @@ -65,7 +74,7 @@ def download_track(service, url, main, fallback=None): dl.download_trackdee( link_track=url, output_dir="./downloads", - quality_download="FLAC", + quality_download=quality, recursive_quality=False, recursive_download=False, method_save=1 diff --git a/static/css/style.css b/static/css/style.css index 3fe730d..083f478 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,3 +1,5 @@ +/* GENERAL STYLING */ + * { margin: 0; padding: 0; @@ -17,9 +19,12 @@ body { padding: 20px; } +/* SEARCH AREA */ + .search-header { display: flex; - gap: 15px; + align-items: center; + gap: 10px; margin-bottom: 30px; position: sticky; top: 0; @@ -62,6 +67,8 @@ body { background-color: #1ed760; } +/* RESULTS GRID */ + .results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); @@ -128,32 +135,66 @@ body { padding: 20px; } -/* Add to your CSS file */ +/* SETTINGS ICON */ + .settings-icon { - position: fixed; - top: 20px; - left: 20px; + position: static; + order: -1; + margin-right: 15px; + font-size: 22px; background: none; border: none; color: #ffffff; - font-size: 24px; cursor: pointer; z-index: 1000; transition: transform 0.3s; } +.settings-icon img, +.queue-icon img { + width: 24px; + height: 24px; + vertical-align: middle; + filter: invert(1); +} + +.settings-icon:hover img, +.queue-icon:hover img { + opacity: 0.8; +} + +/* SIDEBAR */ + .sidebar { position: fixed; top: 0; - left: -350px; width: 350px; height: 100vh; background: #181818; padding: 20px; - transition: left 0.3s; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1001; overflow-y: auto; - color: #ffffff; + box-shadow: 0 0 15px rgba(0,0,0,0.3); +} + +#settingsSidebar { + left: -350px; +} + +#settingsSidebar.active { + left: 0; + box-shadow: 20px 0 30px rgba(0,0,0,0.4); +} + +#downloadQueue { + right: -350px; + left: auto; +} + +#downloadQueue.active { + right: 0; + box-shadow: -20px 0 30px rgba(0,0,0,0.4); } .sidebar.active { @@ -175,6 +216,8 @@ body { cursor: pointer; } +/* SERVICE TABS */ + .service-tabs { display: flex; gap: 10px; @@ -194,6 +237,8 @@ body { background: #1DB954; } +/* CREDENTIALS LIST */ + .credentials-list { margin-bottom: 20px; } @@ -226,6 +271,8 @@ body { color: white; } +/* FORM STYLES */ + .form-group { margin-bottom: 15px; } @@ -236,6 +283,7 @@ body { } .form-group input, +.form-group select, .form-group textarea { width: 100%; padding: 8px; @@ -245,6 +293,17 @@ body { color: white; } +.form-group select { + padding-right: 25px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M5 6L0 0h10z' fill='%23fff'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 10px 6px; +} + .save-btn { background: #1DB954; color: white; @@ -255,18 +314,8 @@ body { width: 100%; } -.deezer-field { - display: none; -} +/* CONFIG BAR */ -.service-tabs { - margin-bottom: 20px; -} - -.service-tabs button.active { - background: #1DB954; -} -/* Add to style.css */ .config-bar { position: fixed; top: 20px; @@ -286,6 +335,8 @@ body { margin-bottom: 5px; } +/* TOGGLE SWITCH */ + .switch { position: relative; display: inline-block; @@ -331,54 +382,22 @@ input:checked + .slider:before { transform: translateX(20px); } -.queue-btn { - background: #1DB954; - color: white; +/* DOWNLOAD QUEUE */ + +.queue-icon { + position: static; + order: 2; + margin-left: 15px; + font-size: 22px; + background: none; border: none; - padding: 8px 15px; - border-radius: 20px; + color: white; cursor: pointer; + z-index: 1000; } - -.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 */ - -#downloadQueue.sidebar { - position: fixed; - top: 0; - right: -350px; - width: 350px; - height: 100vh; - background: #181818; - padding: 20px; - transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 1001; - box-shadow: -20px 0 30px rgba(0,0,0,0.4); -} - -#downloadQueue.sidebar.active { - right: 0; + +.queue-icon:hover { + color: #1DB954; } .queue-header { @@ -396,16 +415,6 @@ input:checked + .slider:before { color: #fff; } -.queue-item .title { - font-size: 14px; - color: #fff; -} - -.queue-item .log { - font-size: 12px; - color: #b3b3b3; -} - .queue-close { background: #2a2a2a; border-radius: 50%; @@ -426,19 +435,6 @@ input:checked + .slider:before { overflow-y: auto; } -#queueItems::-webkit-scrollbar { - width: 6px; -} - -#queueItems::-webkit-scrollbar-track { - background: #181818; -} - -#queueItems::-webkit-scrollbar-thumb { - background: #2a2a2a; - border-radius: 3px; -} - .queue-item { background: #2a2a2a; padding: 15px; @@ -474,13 +470,15 @@ input:checked + .slider:before { font-family: 'SF Mono', Menlo, monospace; } -/* Status message colors */ +/* 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 */ +/* PROGRESS ANIMATION */ + @keyframes progress-pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } @@ -493,7 +491,8 @@ input:checked + .slider:before { animation: progress-pulse 1.5s infinite; } -/* Enhanced loading states */ +/* LOADING SPINNER */ + .loading-spinner { display: inline-block; width: 16px; @@ -508,7 +507,8 @@ input:checked + .slider:before { to { transform: rotate(360deg); } } -/* Timeout warning */ +/* TIMEOUT WARNING */ + .timeout-warning { position: relative; padding-left: 24px; @@ -522,48 +522,8 @@ input:checked + .slider:before { transform: translateY(-50%); } -/* Form enhancements */ -#credentialForm { - margin-top: 20px; -} +/* STATUS BAR ANIMATIONS */ -#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; @@ -572,7 +532,8 @@ input:checked + .slider:before { margin-top: 8px; } -/* Error traceback styling */ +/* ERROR TRACEBACK STYLING */ + .traceback { background: #2a2a2a; padding: 10px; @@ -585,7 +546,8 @@ input:checked + .slider:before { overflow-y: auto; } -/* Queue item states */ +/* QUEUE ITEM STATES */ + .queue-item--complete { border-left: 4px solid #1DB954; } @@ -598,19 +560,16 @@ input:checked + .slider:before { border-left: 4px solid #4a90e2; } -/* Progress percentage styling */ +/* PROGRESS PERCENTAGE STYLING */ + .progress-percent { float: right; font-weight: bold; color: #1DB954; } -/* Hover tooltip for long messages */ -.queue-item .log { - position: relative; -} +/* DOWNLOAD BUTTON */ -/* Download button styling */ .download-btn { background-color: #1DB954; color: white; @@ -645,7 +604,8 @@ input:checked + .slider:before { font-size: 16px; } -/* Select dropdown styling */ +/* ACCOUNT SELECTORS */ + #spotifyAccountSelect, #deezerAccountSelect { background: #2a2a2a; @@ -673,7 +633,6 @@ input:checked + .slider:before { box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.2); } -/* Style the dropdown options */ #spotifyAccountSelect option, #deezerAccountSelect option { background: #181818; @@ -681,344 +640,23 @@ input:checked + .slider:before { 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: - opacity 0.3s ease, - background-color 0.3s ease; - opacity: 1; -} - -.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); - } -} +/* RETRY AND CLOSE BUTTONS */ .retry-btn { padding: 4px 12px; margin: 0 8px; - background-color: #4CAF50; /* Green color for positive action */ + background-color: #4CAF50; color: white; border: none; border-radius: 4px; @@ -1028,7 +666,7 @@ html { } .retry-btn:hover { - background-color: #45a049; /* Darker green on hover */ + background-color: #45a049; } .retry-btn:active { @@ -1036,18 +674,17 @@ html { transform: translateY(1px); } -/* Close button with matching size */ .close-btn { - padding: 4px 12px; /* Match the retry button's padding */ - margin: 0 8px; /* Match the retry button's margin */ + padding: 4px 12px; + margin: 0 8px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; - font-size: 0.9em; /* Match the retry button's font size */ + font-size: 0.9em; transition: background-color 0.3s ease; - line-height: 1; /* Ensure the "×" is vertically centered */ + line-height: 1; } .close-btn:hover { @@ -1059,21 +696,8 @@ html { transform: translateY(1px); } -.settings-icon img, -.queue-icon img { - width: 24px; - height: 24px; - vertical-align: middle; - filter: invert(1); /* Makes icons white */ -} +/* SCROLLBAR STYLING */ -/* Optional: Add hover effects */ -.settings-icon:hover img, -.queue-icon:hover img { - opacity: 0.8; -} - -/* Global Scrollbar Styles */ ::-webkit-scrollbar { width: 10px; height: 10px; @@ -1094,13 +718,11 @@ html { background: #3a3a3a; } -/* Firefox Support */ * { scrollbar-width: thin; scrollbar-color: #2a2a2a #181818; } -/* Sidebar Specific Scrollbars */ .sidebar::-webkit-scrollbar { width: 8px; } @@ -1109,7 +731,6 @@ html { border-width: 1px; } -/* Queue Items Scrollbar */ #queueItems::-webkit-scrollbar { width: 6px; } @@ -1118,17 +739,235 @@ html { background: #3a3a3a; } -/* Results Container Scrollbar */ .results-container::-webkit-scrollbar { width: 8px; } -/* Smooth Scroll Behavior */ html { scroll-behavior: smooth; } -/* Scrollbar Corner */ ::-webkit-scrollbar-corner { background: #121212; +} + +/* MOBILE RESPONSIVENESS */ + +@media (max-width: 768px) { + html { + font-size: 14px; + } + + .container { + padding: 15px; + } + + .search-header { + flex-wrap: wrap; + gap: 12px; + padding: 15px 0; + } + + .settings-icon, + .queue-icon { + margin: 0; + order: 0; + font-size: 24px; + } + + .search-input, + .search-type { + order: 1; + width: 100%; + } + + .search-button { + order: 2; + width: 100%; + } + + .queue-icon { + order: 3; + margin-left: auto; + } + + .results-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 15px; + } + + .result-card { + padding: 12px; + } + + .track-title { + font-size: 14px; + } + + .track-artist { + font-size: 12px; + } + + .sidebar { + width: 100%; + left: -100%; + } + + #settingsSidebar { + left: -100%; + } + + #downloadQueue { + right: -100%; + top: 70px; + } + + .sidebar.active { + left: 0; + } + + #downloadQueue { + width: 90%; + right: 5%; + top: 70px; + } + + .settings-icon { + top: 15px; + left: 15px; + } + + .queue-icon { + top: 15px; + right: 15px; + } + + .form-group input, + .form-group textarea { + padding: 12px; + } + + .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); + } +} + +@media screen and (max-width: 480px) { + .results-grid { + grid-template-columns: 1fr 1fr; + } + + body { + font-size: 16px; + } + + button, + .result-card { + min-height: 48px; + } + + .track-title, + .track-artist { + white-space: normal; + overflow: visible; + text-overflow: clip; + } +} + +input, +select, +textarea { + font-size: 16px !important; +} + +button { + touch-action: manipulation; +} + +.result-card { + -webkit-tap-highlight-color: transparent; +} + +html { + overflow-y: scroll; +} + +/* Add to existing form styles */ +.config-item { + margin-bottom: 1.5rem; +} + +.config-item label { + display: block; + margin-bottom: 0.5rem; + color: #b3b3b3; + font-size: 0.9rem; +} + +/* Quality selectors - match account select style */ +#spotifyQualitySelect, +#deezerQualitySelect { + background: #2a2a2a; + color: white; + border: 1px solid #404040; + border-radius: 8px; + padding: 10px 15px; + font-size: 14px; + width: 100%; + 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: all 0.3s ease; +} + +#spotifyQualitySelect:focus, +#deezerQualitySelect:focus { + outline: none; + border-color: #1DB954; + box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.2); +} + +/* Quality options styling */ +#spotifyQualitySelect option, +#deezerQualitySelect option { + background: #181818; + color: white; + padding: 10px; +} + +/* Config group spacing */ +.account-config { + display: grid; + gap: 1.2rem; + margin-bottom: 2rem; +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .config-item { + margin-bottom: 1rem; + } + + #spotifyQualitySelect, + #deezerQualitySelect { + padding: 12px; + font-size: 15px; + } } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 7f7728c..98984c3 100755 --- a/static/js/app.js +++ b/static/js/app.js @@ -76,23 +76,40 @@ document.addEventListener('DOMContentLoaded', () => { 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', () => { + // Existing listeners + const fallbackToggle = document.getElementById('fallbackToggle'); + if (fallbackToggle) { + fallbackToggle.addEventListener('change', () => { saveConfig(); updateAccountSelectors(); }); + } + + const accountSelects = ['spotifyAccountSelect', 'deezerAccountSelect']; + accountSelects.forEach(id => { + const element = document.getElementById(id); + if (element) { + element.addEventListener('change', () => { + saveConfig(); + updateAccountSelectors(); + }); + } }); + + // Add quality select listeners with null checks + const spotifyQuality = document.getElementById('spotifyQualitySelect'); + if (spotifyQuality) { + spotifyQuality.addEventListener('change', saveConfig); + } + + const deezerQuality = document.getElementById('deezerQualitySelect'); + if (deezerQuality) { + deezerQuality.addEventListener('change', saveConfig); + } } + async function updateAccountSelectors() { try { @@ -273,23 +290,43 @@ async function startDownload(url, type, item) { 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}`; + // Determine service from URL + let service; + if (url.includes('open.spotify.com')) { + service = 'spotify'; + } else if (url.includes('deezer.com')) { + service = 'deezer'; } else { - apiUrl += `&main=${spotifyAccount}`; + showError('Unsupported service URL'); + return; } - + + let apiUrl = `/api/${type}/download?service=${service}&url=${encodeURIComponent(url)}`; + + // Get quality settings + const spotifyQuality = document.getElementById('spotifyQualitySelect').value; + const deezerQuality = document.getElementById('deezerQualitySelect').value; + + if (fallbackEnabled && service === 'spotify') { + // Deezer fallback for Spotify URLs + apiUrl += `&main=${deezerAccount}&fallback=${spotifyAccount}`; + apiUrl += `&quality=${encodeURIComponent(deezerQuality)}`; + apiUrl += `&fall_quality=${encodeURIComponent(spotifyQuality)}`; + } else { + // Standard download without fallback + const mainAccount = service === 'spotify' ? spotifyAccount : deezerAccount; + apiUrl += `&main=${mainAccount}`; + apiUrl += `&quality=${encodeURIComponent(service === 'spotify' ? spotifyQuality : deezerQuality)}`; + } + try { - const response = await fetch(apiUrl); - const data = await response.json(); - - addToQueue(item, type, data.prg_file); + const response = await fetch(apiUrl); + const data = await response.json(); + addToQueue(item, type, data.prg_file); } catch (error) { - showError('Download failed: ' + error.message); + showError('Download failed: ' + error.message); } -} + } function addToQueue(item, type, prgFile) { const queueId = Date.now().toString() + Math.random().toString(36).substr(2, 9); @@ -630,9 +667,11 @@ function getStatusMessage(data) { function saveConfig() { const config = { - spotify: document.getElementById('spotifyAccountSelect').value, - deezer: document.getElementById('deezerAccountSelect').value, - fallback: document.getElementById('fallbackToggle').checked + spotify: document.getElementById('spotifyAccountSelect').value, + deezer: document.getElementById('deezerAccountSelect').value, + fallback: document.getElementById('fallbackToggle').checked, + spotifyQuality: document.getElementById('spotifyQualitySelect').value, + deezerQuality: document.getElementById('deezerQualitySelect').value }; localStorage.setItem('activeConfig', JSON.stringify(config)); } @@ -641,16 +680,24 @@ function saveConfig() { function loadConfig() { const saved = JSON.parse(localStorage.getItem('activeConfig')) || {}; - // Set values only if they exist in the DOM + // Account selects const spotifySelect = document.getElementById('spotifyAccountSelect'); - const deezerSelect = document.getElementById('deezerAccountSelect'); - if (spotifySelect) spotifySelect.value = saved.spotify || ''; + + const deezerSelect = document.getElementById('deezerAccountSelect'); if (deezerSelect) deezerSelect.value = saved.deezer || ''; + // Fallback toggle const fallbackToggle = document.getElementById('fallbackToggle'); if (fallbackToggle) fallbackToggle.checked = !!saved.fallback; -} + + // Quality selects + const spotifyQuality = document.getElementById('spotifyQualitySelect'); + if (spotifyQuality) spotifyQuality.value = saved.spotifyQuality || 'NORMAL'; + + const deezerQuality = document.getElementById('deezerQualitySelect'); + if (deezerQuality) deezerQuality.value = saved.deezerQuality || 'MP3_128'; +} function isSpotifyUrl(url) { return url.startsWith('https://open.spotify.com/'); diff --git a/templates/index.html b/templates/index.html index 4d13abd..d178065 100755 --- a/templates/index.html +++ b/templates/index.html @@ -19,10 +19,26 @@ +