From c2e526be603b57c56d9d89491cacd3c1fb9b1b59 Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Tue, 3 Jun 2025 22:37:45 -0600 Subject: [PATCH] implemeented file format convertion --- requirements.txt | 2 +- routes/utils/album.py | 20 ++++-- routes/utils/celery_config.py | 4 +- routes/utils/celery_queue_manager.py | 10 ++- routes/utils/celery_tasks.py | 16 ++++- routes/utils/playlist.py | 16 ++++- routes/utils/track.py | 20 ++++-- src/js/config.ts | 96 +++++++++++++++++++++++++++- static/html/config.html | 30 +++++++++ 9 files changed, 194 insertions(+), 20 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5aae6f9..3d91bcd 100755 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ waitress==3.0.2 celery==5.5.3 Flask==3.1.1 flask_cors==6.0.0 -deezspot-spotizerr==1.4.1 +deezspot-spotizerr==1.5.0 diff --git a/routes/utils/album.py b/routes/utils/album.py index ef17495..9440df7 100755 --- a/routes/utils/album.py +++ b/routes/utils/album.py @@ -19,7 +19,9 @@ def download_album( initial_retry_delay=5, retry_delay_increase=5, max_retries=3, - progress_callback=None + progress_callback=None, + convert_to=None, + bitrate=None ): try: # Detect URL source (Spotify or Deezer) from URL @@ -118,7 +120,9 @@ def download_album( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Album download completed successfully using Deezer credentials") except Exception as e: @@ -160,7 +164,9 @@ def download_album( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Album download completed successfully using Spotify fallback") except Exception as e2: @@ -201,7 +207,9 @@ def download_album( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Album download completed successfully using Spotify main") # For Deezer URLs: download directly from Deezer @@ -236,7 +244,9 @@ def download_album( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Album download completed successfully using Deezer direct") else: diff --git a/routes/utils/celery_config.py b/routes/utils/celery_config.py index 77bb47b..fce5711 100644 --- a/routes/utils/celery_config.py +++ b/routes/utils/celery_config.py @@ -39,7 +39,9 @@ DEFAULT_MAIN_CONFIG = { 'maxConcurrentDownloads': 3, 'maxRetries': 3, 'retryDelaySeconds': 5, - 'retry_delay_increase': 5 + 'retry_delay_increase': 5, + 'convertTo': None, + 'bitrate': None } def get_config_params(): diff --git a/routes/utils/celery_queue_manager.py b/routes/utils/celery_queue_manager.py index b5d3d96..dc637fa 100644 --- a/routes/utils/celery_queue_manager.py +++ b/routes/utils/celery_queue_manager.py @@ -60,7 +60,9 @@ def get_config_params(): 'save_cover': config.get('save_cover', True), 'maxRetries': config.get('maxRetries', 3), 'retryDelaySeconds': config.get('retryDelaySeconds', 5), - 'retry_delay_increase': config.get('retry_delay_increase', 5) + 'retry_delay_increase': config.get('retry_delay_increase', 5), + 'convertTo': config.get('convertTo', None), + 'bitrate': config.get('bitrate', None) } except Exception as e: logger.error(f"Error reading config for parameters: {e}") @@ -78,7 +80,9 @@ def get_config_params(): 'save_cover': True, 'maxRetries': 3, 'retryDelaySeconds': 5, - 'retry_delay_increase': 5 + 'retry_delay_increase': 5, + 'convertTo': None, # Default for conversion + 'bitrate': None # Default for bitrate } class CeleryDownloadQueueManager: @@ -201,6 +205,8 @@ class CeleryDownloadQueueManager: "custom_track_format": original_request.get("custom_track_format", config_params['customTrackFormat']), "pad_tracks": self._parse_bool_param(original_request.get("tracknum_padding"), config_params['tracknum_padding']), "save_cover": self._parse_bool_param(original_request.get("save_cover"), config_params['save_cover']), + "convertTo": original_request.get("convertTo", config_params.get('convertTo')), + "bitrate": original_request.get("bitrate", config_params.get('bitrate')), "retry_count": 0, "original_request": original_request, "created_at": time.time() diff --git a/routes/utils/celery_tasks.py b/routes/utils/celery_tasks.py index b5c857d..0f88897 100644 --- a/routes/utils/celery_tasks.py +++ b/routes/utils/celery_tasks.py @@ -1087,6 +1087,8 @@ def download_track(self, **task_data): custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%")) pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True)) save_cover = task_data.get("save_cover", config_params.get("save_cover", True)) + convert_to = task_data.get("convertTo", config_params.get("convertTo")) + bitrate = task_data.get("bitrate", config_params.get("bitrate")) # Execute the download - service is now determined from URL download_track_func( @@ -1100,7 +1102,9 @@ def download_track(self, **task_data): custom_track_format=custom_track_format, pad_tracks=pad_tracks, save_cover=save_cover, - progress_callback=self.progress_callback + progress_callback=self.progress_callback, + convert_to=convert_to, + bitrate=bitrate ) return {"status": "success", "message": "Track download completed"} @@ -1156,6 +1160,8 @@ def download_album(self, **task_data): custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%")) pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True)) save_cover = task_data.get("save_cover", config_params.get("save_cover", True)) + convert_to = task_data.get("convertTo", config_params.get("convertTo")) + bitrate = task_data.get("bitrate", config_params.get("bitrate")) # Execute the download - service is now determined from URL download_album_func( @@ -1169,7 +1175,9 @@ def download_album(self, **task_data): custom_track_format=custom_track_format, pad_tracks=pad_tracks, save_cover=save_cover, - progress_callback=self.progress_callback + progress_callback=self.progress_callback, + convert_to=convert_to, + bitrate=bitrate ) return {"status": "success", "message": "Album download completed"} @@ -1225,6 +1233,8 @@ def download_playlist(self, **task_data): custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%")) pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True)) save_cover = task_data.get("save_cover", config_params.get("save_cover", True)) + convert_to = task_data.get("convertTo", config_params.get("convertTo")) + bitrate = task_data.get("bitrate", config_params.get("bitrate")) # Get retry parameters initial_retry_delay = task_data.get("initial_retry_delay", config_params.get("retryDelaySeconds", 5)) @@ -1247,6 +1257,8 @@ def download_playlist(self, **task_data): retry_delay_increase=retry_delay_increase, max_retries=max_retries, progress_callback=self.progress_callback, + convert_to=convert_to, + bitrate=bitrate ) return {"status": "success", "message": "Playlist download completed"} diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index cce34fd..3df0684 100755 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -20,6 +20,8 @@ def download_playlist( retry_delay_increase=5, max_retries=3, progress_callback=None, + convert_to=None, + bitrate=None ): try: # Detect URL source (Spotify or Deezer) from URL @@ -114,6 +116,8 @@ def download_playlist( initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Playlist download completed successfully using Deezer credentials") except Exception as e: @@ -155,7 +159,9 @@ def download_playlist( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Playlist download completed successfully using Spotify fallback") except Exception as e2: @@ -196,7 +202,9 @@ def download_playlist( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Playlist download completed successfully using Spotify main") # For Deezer URLs: download directly from Deezer @@ -231,7 +239,9 @@ def download_playlist( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) print(f"DEBUG: Playlist download completed successfully using Deezer direct") else: diff --git a/routes/utils/track.py b/routes/utils/track.py index 97e9e0d..7d9cdd1 100755 --- a/routes/utils/track.py +++ b/routes/utils/track.py @@ -19,7 +19,9 @@ def download_track( initial_retry_delay=5, retry_delay_increase=5, max_retries=3, - progress_callback=None + progress_callback=None, + convert_to=None, + bitrate=None ): try: # Detect URL source (Spotify or Deezer) from URL @@ -112,7 +114,9 @@ def download_track( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) except Exception as e: deezer_error = e @@ -148,7 +152,9 @@ def download_track( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) except Exception as e2: # If fallback also fails, raise an error indicating both attempts failed @@ -182,7 +188,9 @@ def download_track( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) # For Deezer URLs: download directly from Deezer elif service == 'deezer': @@ -211,7 +219,9 @@ def download_track( save_cover=save_cover, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + convert_to=convert_to, + bitrate=bitrate ) else: raise ValueError(f"Unsupported service: {service}") diff --git a/src/js/config.ts b/src/js/config.ts index 6146d07..053f631 100644 --- a/src/js/config.ts +++ b/src/js/config.ts @@ -53,6 +53,17 @@ let isEditingSearch = false; let activeSpotifyAccount = ''; let activeDeezerAccount = ''; +// Define available formats and their bitrates +const CONVERSION_FORMATS: Record = { + MP3: ['32k', '64k', '96k', '128k', '192k', '256k', '320k'], + AAC: ['32k', '64k', '96k', '128k', '192k', '256k'], + OGG: ['64k', '96k', '128k', '192k', '256k', '320k'], + OPUS: ['32k', '64k', '96k', '128k', '192k', '256k'], + FLAC: [], // No specific bitrates + WAV: [], // No specific bitrates + ALAC: [] // No specific bitrates +}; + // Reference to the credentials form card and add button let credentialsFormCard: HTMLElement | null = null; let showAddAccountFormBtn: HTMLElement | null = null; @@ -124,6 +135,23 @@ async function loadConfig() { const saveCoverToggle = document.getElementById('saveCoverToggle') as HTMLInputElement | null; if (saveCoverToggle) saveCoverToggle.checked = savedConfig.save_cover === undefined ? true : !!savedConfig.save_cover; + // Load conversion settings + const convertToSelect = document.getElementById('convertToSelect') as HTMLSelectElement | null; + if (convertToSelect) { + convertToSelect.value = savedConfig.convertTo || ''; + updateBitrateOptions(convertToSelect.value); + } + const bitrateSelect = document.getElementById('bitrateSelect') as HTMLSelectElement | null; + if (bitrateSelect && savedConfig.bitrate) { + if (Array.from(bitrateSelect.options).some(option => option.value === savedConfig.bitrate)) { + bitrateSelect.value = savedConfig.bitrate; + } + } else if (bitrateSelect) { + if (convertToSelect && !CONVERSION_FORMATS[convertToSelect.value]?.length) { + bitrateSelect.value = ''; + } + } + // Update explicit filter status updateExplicitFilterStatus(savedConfig.explicitFilter); @@ -249,6 +277,13 @@ function setupEventListeners() { (document.getElementById('maxRetries') as HTMLInputElement | null)?.addEventListener('change', saveConfig); (document.getElementById('retryDelaySeconds') as HTMLInputElement | null)?.addEventListener('change', saveConfig); + // Conversion settings listeners + (document.getElementById('convertToSelect') as HTMLSelectElement | null)?.addEventListener('change', function() { + updateBitrateOptions(this.value); + saveConfig(); + }); + (document.getElementById('bitrateSelect') as HTMLSelectElement | null)?.addEventListener('change', saveConfig); + // Update active account globals when the account selector is changed. (document.getElementById('spotifyAccountSelect') as HTMLSelectElement | null)?.addEventListener('change', (e: Event) => { activeSpotifyAccount = (e.target as HTMLSelectElement).value; @@ -821,6 +856,13 @@ function resetForm() { } (document.getElementById('credentialForm') as HTMLFormElement | null)?.reset(); + // Reset conversion dropdowns to ensure bitrate is updated correctly + const convertToSelect = document.getElementById('convertToSelect') as HTMLSelectElement | null; + if (convertToSelect) { + convertToSelect.value = ''; // Reset to 'No Conversion' + updateBitrateOptions(''); // Update bitrate for 'No Conversion' + } + // Reset form title and button text const serviceName = currentService.charAt(0).toUpperCase() + currentService.slice(1); (document.getElementById('formTitle') as HTMLElement | null)!.textContent = `Add New ${serviceName} Account`; @@ -847,7 +889,9 @@ async function saveConfig() { retryDelaySeconds: parseInt((document.getElementById('retryDelaySeconds') as HTMLInputElement | null)?.value || '5', 10) || 5, retry_delay_increase: parseInt((document.getElementById('retryDelayIncrease') as HTMLInputElement | null)?.value || '5', 10) || 5, tracknum_padding: (document.getElementById('tracknumPaddingToggle') as HTMLInputElement | null)?.checked, - save_cover: (document.getElementById('saveCoverToggle') as HTMLInputElement | null)?.checked + save_cover: (document.getElementById('saveCoverToggle') as HTMLInputElement | null)?.checked, + convertTo: (document.getElementById('convertToSelect') as HTMLSelectElement | null)?.value || null, // Get convertTo value + bitrate: (document.getElementById('bitrateSelect') as HTMLSelectElement | null)?.value || null // Get bitrate value }; try { @@ -908,6 +952,23 @@ async function saveConfig() { const saveCoverToggle = document.getElementById('saveCoverToggle') as HTMLInputElement | null; if (saveCoverToggle) saveCoverToggle.checked = savedConfig.save_cover === undefined ? true : !!savedConfig.save_cover; + // Load conversion settings after save + const convertToSelect = document.getElementById('convertToSelect') as HTMLSelectElement | null; + if (convertToSelect) { + convertToSelect.value = savedConfig.convertTo || ''; + updateBitrateOptions(convertToSelect.value); + } + const bitrateSelect = document.getElementById('bitrateSelect') as HTMLSelectElement | null; + if (bitrateSelect && savedConfig.bitrate) { + if (Array.from(bitrateSelect.options).some(option => option.value === savedConfig.bitrate)) { + bitrateSelect.value = savedConfig.bitrate; + } + } else if (bitrateSelect) { + if (convertToSelect && !CONVERSION_FORMATS[convertToSelect.value]?.length) { + bitrateSelect.value = ''; + } + } + // Update explicit filter status updateExplicitFilterStatus(savedConfig.explicitFilter); @@ -1087,3 +1148,36 @@ function updateWatchWarningDisplay() { firstEnableNoticeDiv.style.display = 'none'; } } + +// Function to update bitrate options based on selected format +function updateBitrateOptions(selectedFormat: string) { + const bitrateSelect = document.getElementById('bitrateSelect') as HTMLSelectElement | null; + if (!bitrateSelect) return; + + bitrateSelect.innerHTML = ''; // Clear existing options + const currentBitrateValue = bitrateSelect.value; // Preserve current value if possible + + if (selectedFormat && CONVERSION_FORMATS[selectedFormat] && CONVERSION_FORMATS[selectedFormat].length > 0) { + bitrateSelect.disabled = false; + CONVERSION_FORMATS[selectedFormat].forEach(bRate => { + const option = document.createElement('option'); + option.value = bRate; + option.textContent = bRate; + bitrateSelect.appendChild(option); + }); + // Try to restore previous valid bitrate or set to first available + if (CONVERSION_FORMATS[selectedFormat].includes(currentBitrateValue)) { + bitrateSelect.value = currentBitrateValue; + } else { + bitrateSelect.value = CONVERSION_FORMATS[selectedFormat][0]; // Default to first available bitrate + } + } else { + // For formats with no specific bitrates (FLAC, WAV, ALAC) or 'No Conversion' + bitrateSelect.disabled = true; + const option = document.createElement('option'); + option.value = ''; + option.textContent = 'N/A'; + bitrateSelect.appendChild(option); + bitrateSelect.value = ''; + } +} diff --git a/static/html/config.html b/static/html/config.html index ce5d8f4..6a8677e 100644 --- a/static/html/config.html +++ b/static/html/config.html @@ -86,6 +86,36 @@ + + +

Conversion Settings

+
+ + +
+ Select a format to convert downloaded files to. "No Conversion" keeps the original format. +
+
+
+ + +
+ Select the bitrate for the chosen format. Only applicable for lossy formats. +
+
+

Retry Options