import sys import os from dataclasses import dataclass from datetime import datetime from pathlib import Path import requests import re import asyncio from packaging import version import qdarktheme from PyQt6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton, QAbstractItemView, QProgressBar, QCheckBox, QDialog, QDialogButtonBox, QComboBox, QStyledItemDelegate ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException from qobuzAutoDL import QobuzDownloader as QobuzAutoDownloader from qobuzRegionDL import QobuzDownloader as QobuzRegionDownloader from tidalDL import TidalDownloader from deezerDL import DeezerDownloader from amazonDL import LucidaDownloader @dataclass class Track: external_urls: str title: str artists: str album: str track_number: int duration_ms: int id: str isrc: str = "" release_date: str = "" class MetadataFetchWorker(QThread): finished = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, url): super().__init__() self.url = url def run(self): try: metadata = get_filtered_data(self.url) if "error" in metadata: self.error.emit(metadata["error"]) else: self.finished.emit(metadata) except SpotifyInvalidUrlException as e: self.error.emit(str(e)) except Exception as e: self.error.emit(f'Failed to fetch metadata: {str(e)}') class DownloadWorker(QThread): finished = pyqtSignal(bool, str, list, list, list) progress = pyqtSignal(str, int) def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False, album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True, use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us", qobuz_mode="auto"): super().__init__() self.tracks = tracks self.outpath = outpath self.is_single_track = is_single_track self.is_album = is_album self.is_playlist = is_playlist self.album_or_playlist_name = album_or_playlist_name self.filename_format = filename_format self.use_track_numbers = use_track_numbers self.use_artist_subfolders = use_artist_subfolders self.use_album_subfolders = use_album_subfolders self.service = service self.qobuz_region = qobuz_region self.qobuz_mode = qobuz_mode self.is_paused = False self.is_stopped = False self.failed_tracks = [] self.successful_tracks = [] self.skipped_tracks = [] def get_formatted_filename(self, track): if self.filename_format == "artist_title": filename = f"{track.artists} - {track.title}.flac" elif self.filename_format == "title_only": filename = f"{track.title}.flac" else: filename = f"{track.title} - {track.artists}.flac" return re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', filename) def run(self): try: if self.service == "qobuz": if self.qobuz_mode == "auto": downloader = QobuzAutoDownloader() else: downloader = QobuzRegionDownloader(self.qobuz_region) elif self.service == "tidal": downloader = TidalDownloader() elif self.service == "deezer": downloader = DeezerDownloader() elif self.service == "amazon": downloader = LucidaDownloader() else: downloader = TidalDownloader() def progress_update(current, total): if total <= 0: self.progress.emit("Processing metadata...", 0) downloader.set_progress_callback(progress_update) total_tracks = len(self.tracks) for i, track in enumerate(self.tracks): while self.is_paused: if self.is_stopped: return self.msleep(100) if self.is_stopped: return self.progress.emit(f"Starting download ({i+1}/{total_tracks}): {track.title} - {track.artists}", int((i) / total_tracks * 100)) try: if self.is_playlist: track_outpath = self.outpath if self.use_artist_subfolders: artist_name = track.artists.split(', ')[0] if ', ' in track.artists else track.artists artist_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', artist_name) track_outpath = os.path.join(track_outpath, artist_folder) if self.use_album_subfolders: album_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', track.album) track_outpath = os.path.join(track_outpath, album_folder) os.makedirs(track_outpath, exist_ok=True) else: track_outpath = self.outpath if (self.is_album or self.is_playlist) and self.use_track_numbers: new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}" else: new_filename = self.get_formatted_filename(track) new_filename = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', new_filename) new_filepath = os.path.join(track_outpath, new_filename) if os.path.exists(new_filepath) and os.path.getsize(new_filepath) > 0: self.progress.emit(f"File already exists: {new_filename}. Skipping download.", 0) self.progress.emit(f"Skipped: {track.title} - {track.artists}", int((i + 1) / total_tracks * 100)) self.skipped_tracks.append(track) continue if self.service == "qobuz": if not track.isrc: self.progress.emit(f"No ISRC found for track: {track.title}. Skipping.", 0) self.failed_tracks.append((track.title, track.artists, "No ISRC available")) continue self.progress.emit(f"Getting track from Qobuz with ISRC: {track.isrc}", 0) is_paused_callback = lambda: self.is_paused is_stopped_callback = lambda: self.is_stopped downloaded_file = downloader.download( track.isrc, track_outpath, is_paused_callback=is_paused_callback, is_stopped_callback=is_stopped_callback ) elif self.service == "tidal": if not track.isrc: self.progress.emit(f"No ISRC found for track: {track.title}. Skipping.", 0) self.failed_tracks.append((track.title, track.artists, "No ISRC available")) continue self.progress.emit(f"Searching and downloading from Tidal for ISRC: {track.isrc} - {track.title} - {track.artists}", 0) is_paused_callback = lambda: self.is_paused is_stopped_callback = lambda: self.is_stopped download_result_details = downloader.download( query=f"{track.title} {track.artists}", isrc=track.isrc, output_dir=track_outpath, quality="LOSSLESS", is_paused_callback=is_paused_callback, is_stopped_callback=is_stopped_callback ) if isinstance(download_result_details, str) and os.path.exists(download_result_details): downloaded_file = download_result_details elif isinstance(download_result_details, dict) and download_result_details.get("success") == False and download_result_details.get("error") == "Download stopped by user": self.progress.emit(f"Download stopped by user for: {track.title}",0) return elif isinstance(download_result_details, dict) and download_result_details.get("success") == False: raise Exception(download_result_details.get("error", "Tidal download failed")) elif isinstance(download_result_details, dict) and (download_result_details.get("status") == "all_skipped" or download_result_details.get("status") == "skipped_exists"): self.progress.emit(f"File already exists or skipped: {new_filename}",0) downloaded_file = new_filepath self.skipped_tracks.append(track) else: downloaded_file = None raise Exception(f"Tidal download failed or returned unexpected result: {download_result_details}") elif self.service == "deezer": if not track.isrc: self.progress.emit(f"No ISRC found for track: {track.title}. Skipping.", 0) self.failed_tracks.append((track.title, track.artists, "No ISRC available")) continue self.progress.emit(f"Downloading from Deezer with ISRC: {track.isrc}", 0) success = asyncio.run(downloader.download_by_isrc(track.isrc, track_outpath)) if success: safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '-', '_')).rstrip() safe_artist = "".join(c for c in track.artists if c.isalnum() or c in (' ', '-', '_')).rstrip() expected_filename = f"{safe_artist} - {safe_title}.flac" downloaded_file = os.path.join(track_outpath, expected_filename) if not os.path.exists(downloaded_file): import glob flac_files = glob.glob(os.path.join(track_outpath, "*.flac")) if flac_files: downloaded_file = max(flac_files, key=os.path.getctime) else: raise Exception("Downloaded file not found") else: raise Exception("Deezer download failed") elif self.service == "amazon": self.progress.emit(f"Downloading from Amazon Music: {track.title} - {track.artists}", 0) is_paused_callback = lambda: self.is_paused is_stopped_callback = lambda: self.is_stopped downloaded_file = downloader.download( track.id, track_outpath, is_paused_callback=is_paused_callback, is_stopped_callback=is_stopped_callback ) if not downloaded_file or not os.path.exists(downloaded_file): raise Exception("Amazon Music download failed") else: track_id = track.id self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0) try: loop = asyncio.get_event_loop() if loop.is_closed(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) metadata = loop.run_until_complete(downloader.get_track_info(track_id, self.service)) self.progress.emit(f"Track info received, starting download process", 0) is_paused_callback = lambda: self.is_paused is_stopped_callback = lambda: self.is_stopped downloaded_file = downloader.download( metadata, track_outpath, is_paused_callback=is_paused_callback, is_stopped_callback=is_stopped_callback ) if self.is_stopped: return if downloaded_file and os.path.exists(downloaded_file): if downloaded_file == new_filepath: self.progress.emit(f"File already exists: {new_filename}", 0) self.progress.emit(f"Skipped: {track.title} - {track.artists}", int((i + 1) / total_tracks * 100)) self.skipped_tracks.append(track) continue if downloaded_file != new_filepath: try: os.rename(downloaded_file, new_filepath) self.progress.emit(f"File renamed to: {new_filename}", 0) except OSError as e: self.progress.emit(f"Warning: Could not rename file {downloaded_file} to {new_filepath}: {str(e)}", 0) pass else: raise Exception(f"Download failed or file not found: {downloaded_file}") self.progress.emit(f"Successfully downloaded: {track.title} - {track.artists}", int((i + 1) / total_tracks * 100)) self.successful_tracks.append(track) except Exception as e: self.failed_tracks.append((track.title, track.artists, str(e))) self.progress.emit(f"Failed to download: {track.title} - {track.artists}\nError: {str(e)}", int((i + 1) / total_tracks * 100)) continue if not self.is_stopped: success_message = "Download completed!" if self.failed_tracks: success_message += f"\n\nFailed downloads: {len(self.failed_tracks)} tracks" if self.successful_tracks: success_message += f"\n\nSuccessful downloads: {len(self.successful_tracks)} tracks" if self.skipped_tracks: success_message += f"\n\nSkipped (already exists): {len(self.skipped_tracks)} tracks" self.finished.emit(True, success_message, self.failed_tracks, self.successful_tracks, self.skipped_tracks) except Exception as e: self.finished.emit(False, str(e), self.failed_tracks, self.successful_tracks, self.skipped_tracks) def pause(self): self.is_paused = True self.progress.emit("Download process paused.", 0) def resume(self): self.is_paused = False self.progress.emit("Download process resumed.", 0) def stop(self): self.is_stopped = True self.is_paused = False class UpdateDialog(QDialog): def __init__(self, current_version, new_version, parent=None): super().__init__(parent) self.setWindowTitle("Update Now") self.setFixedWidth(400) self.setModal(True) layout = QVBoxLayout() message = QLabel(f"SpotiFLAC v{new_version} Available!") message.setWordWrap(True) layout.addWidget(message) button_box = QDialogButtonBox() self.update_button = QPushButton("Check") self.update_button.setCursor(Qt.CursorShape.PointingHandCursor) self.cancel_button = QPushButton("Later") self.cancel_button.setCursor(Qt.CursorShape.PointingHandCursor) button_box.addButton(self.update_button, QDialogButtonBox.ButtonRole.AcceptRole) button_box.addButton(self.cancel_button, QDialogButtonBox.ButtonRole.RejectRole) layout.addWidget(button_box) self.setLayout(layout) self.update_button.clicked.connect(self.accept) self.cancel_button.clicked.connect(self.reject) class TidalStatusChecker(QThread): status_updated = pyqtSignal(bool) error = pyqtSignal(str) def run(self): try: response = requests.get("https://tidal.401658.xyz", timeout=5) is_online = response.status_code == 200 or response.status_code == 429 self.status_updated.emit(is_online) except Exception as e: self.error.emit(f"Error checking Tidal (API) status: {str(e)}") self.status_updated.emit(False) class QobuzStatusChecker(QThread): status_updated = pyqtSignal(bool) error = pyqtSignal(str) def __init__(self, region="us", mode="auto"): super().__init__() self.region = region self.mode = mode def run(self): try: if self.mode == "auto": response = requests.get("https://qobuz.squid.wtf", timeout=5) else: response = requests.get(f"https://{self.region}.qqdl.site", timeout=5) self.status_updated.emit(response.status_code == 200) except Exception as e: self.error.emit(f"Error checking Qobuz status: {str(e)}") self.status_updated.emit(False) class DeezerStatusChecker(QThread): status_updated = pyqtSignal(bool) error = pyqtSignal(str) def run(self): try: response = requests.get("https://deezmate.com/", timeout=5) is_online = response.status_code == 200 self.status_updated.emit(is_online) except Exception as e: self.error.emit(f"Error checking Deezer status: {str(e)}") self.status_updated.emit(False) class AmazonStatusChecker(QThread): status_updated = pyqtSignal(bool) error = pyqtSignal(str) def run(self): try: response = requests.get("https://lucida.to/api/load?url=%2Fapi%2Fcountries%3Fservice%3Damazon", timeout=5) is_online = response.status_code == 200 self.status_updated.emit(is_online) except Exception as e: self.error.emit(f"Error checking Amazon Music status: {str(e)}") self.status_updated.emit(False) class StatusIndicatorDelegate(QStyledItemDelegate): def paint(self, painter, option, index): item_data = index.data(Qt.ItemDataRole.UserRole) is_online = item_data.get('online', False) if item_data else False super().paint(painter, option, index) indicator_color = Qt.GlobalColor.green if is_online else Qt.GlobalColor.red circle_size = 6 circle_y = option.rect.center().y() - circle_size // 2 circle_x = option.rect.right() - circle_size - 5 painter.save() painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QBrush(indicator_color)) painter.drawEllipse(circle_x, circle_y, circle_size, circle_size) painter.restore() class ServiceComboBox(QComboBox): def __init__(self, parent=None): super().__init__(parent) self.setIconSize(QSize(16, 16)) self.services_status = {} self.setItemDelegate(StatusIndicatorDelegate()) self.setup_items() self.tidal_status_checker = TidalStatusChecker() self.tidal_status_checker.status_updated.connect(self.update_tidal_service_status) self.tidal_status_checker.error.connect(lambda e: print(f"Tidal status check error: {e}")) self.tidal_status_checker.start() self.tidal_status_timer = QTimer(self) self.tidal_status_timer.timeout.connect(self.refresh_tidal_status) self.tidal_status_timer.start(60000) self.deezer_status_checker = DeezerStatusChecker() self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status) self.deezer_status_checker.error.connect(lambda e: print(f"Deezer status check error: {e}")) self.deezer_status_checker.start() self.deezer_status_timer = QTimer(self) self.deezer_status_timer.timeout.connect(self.refresh_deezer_status) self.deezer_status_timer.start(60000) self.amazon_status_checker = AmazonStatusChecker() self.amazon_status_checker.status_updated.connect(self.update_amazon_service_status) self.amazon_status_checker.error.connect(lambda e: print(f"Amazon Music status check error: {e}")) self.amazon_status_checker.start() self.amazon_status_timer = QTimer(self) self.amazon_status_timer.timeout.connect(self.refresh_amazon_status) self.amazon_status_timer.start(60000) def setup_items(self): current_dir = os.path.dirname(os.path.abspath(__file__)) self.services = [ {'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False}, {'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False}, {'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False}, {'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png', 'online': False} ] for service in self.services: icon_path = os.path.join(current_dir, service['icon']) if not os.path.exists(icon_path): self.create_placeholder_icon(icon_path) icon = QIcon(icon_path) self.addItem(icon, service['name']) item_index = self.count() - 1 self.setItemData(item_index, service['id'], Qt.ItemDataRole.UserRole + 1) self.setItemData(item_index, service, Qt.ItemDataRole.UserRole) def create_placeholder_icon(self, path): pixmap = QPixmap(16, 16) pixmap.fill(Qt.GlobalColor.transparent) pixmap.save(path) def update_service_status(self, service_id, is_online): for i in range(self.count()): current_service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) if current_service_id == service_id: service_data = self.itemData(i, Qt.ItemDataRole.UserRole) if isinstance(service_data, dict): service_data['online'] = is_online self.setItemData(i, service_data, Qt.ItemDataRole.UserRole) break self.update() def update_tidal_service_status(self, is_online): self.update_service_status('tidal', is_online) def refresh_tidal_status(self): if hasattr(self, 'tidal_status_checker') and self.tidal_status_checker.isRunning(): self.tidal_status_checker.quit() self.tidal_status_checker.wait() self.tidal_status_checker = TidalStatusChecker() self.tidal_status_checker.status_updated.connect(self.update_tidal_service_status) self.tidal_status_checker.error.connect(lambda e: print(f"Tidal status check error: {e}")) self.tidal_status_checker.start() def update_deezer_service_status(self, is_online): self.update_service_status('deezer', is_online) def refresh_deezer_status(self): if hasattr(self, 'deezer_status_checker') and self.deezer_status_checker.isRunning(): self.deezer_status_checker.quit() self.deezer_status_checker.wait() self.deezer_status_checker = DeezerStatusChecker() self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status) self.deezer_status_checker.error.connect(lambda e: print(f"Deezer status check error: {e}")) self.deezer_status_checker.start() def update_amazon_service_status(self, is_online): self.update_service_status('amazon', is_online) def refresh_amazon_status(self): if hasattr(self, 'amazon_status_checker') and self.amazon_status_checker.isRunning(): self.amazon_status_checker.quit() self.amazon_status_checker.wait() self.amazon_status_checker = AmazonStatusChecker() self.amazon_status_checker.status_updated.connect(self.update_amazon_service_status) self.amazon_status_checker.error.connect(lambda e: print(f"Amazon Music status check error: {e}")) self.amazon_status_checker.start() def currentData(self, role=Qt.ItemDataRole.UserRole + 1): return super().currentData(role) def update_qobuz_status(self, region_id, is_online): for i in range(self.count()): service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) if service_id == 'qobuz': service_data = self.itemData(i, Qt.ItemDataRole.UserRole) if isinstance(service_data, dict): if is_online or service_data.get('online', False): service_data['online'] = True self.setItemData(i, service_data, Qt.ItemDataRole.UserRole) break self.update() class QobuzRegionComboBox(QComboBox): status_updated = pyqtSignal(str, bool) def __init__(self, parent=None): super().__init__(parent) self.setIconSize(QSize(16, 16)) self.setItemDelegate(StatusIndicatorDelegate()) self.setup_items() self.status_checkers = {} self.check_status() self.status_timer = QTimer(self) self.status_timer.timeout.connect(self.check_status) self.status_timer.start(60000) def setup_items(self): current_dir = os.path.dirname(os.path.abspath(__file__)) self.regions = [ {'id': 'us', 'name': 'USA', 'icon': 'us.svg', 'online': False}, {'id': 'eu', 'name': 'Europe', 'icon': 'eu.svg', 'online': False}, {'id': 'br', 'name': 'Brazil', 'icon': 'br.svg', 'online': False}, {'id': 'jp', 'name': 'Japan', 'icon': 'jp.svg', 'online': False}, {'id': 'au', 'name': 'Australia', 'icon': 'au.svg', 'online': False} ] for region in self.regions: icon_path = os.path.join(current_dir, region['icon']) if not os.path.exists(icon_path): self.create_placeholder_icon(icon_path) icon = QIcon(icon_path) self.addItem(icon, region['name']) item_index = self.count() - 1 self.setItemData(item_index, region['id'], Qt.ItemDataRole.UserRole + 1) self.setItemData(item_index, region, Qt.ItemDataRole.UserRole) def create_placeholder_icon(self, path): pixmap = QPixmap(16, 16) pixmap.fill(Qt.GlobalColor.transparent) pixmap.save(path) def update_region_status(self, region_id, is_online): for i in range(self.count()): current_region_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) if current_region_id == region_id: region_data = self.itemData(i, Qt.ItemDataRole.UserRole) if isinstance(region_data, dict): region_data['online'] = is_online self.setItemData(i, region_data, Qt.ItemDataRole.UserRole) break self.update() def check_status(self): for region_id, checker in self.status_checkers.items(): if checker.isRunning(): checker.quit() checker.wait() self.status_checkers.clear() for region in self.regions: region_id = region['id'] checker = QobuzStatusChecker(region_id, "region") checker.status_updated.connect(lambda status, rid=region_id: self.handle_status_update(rid, status)) checker.start() self.status_checkers[region_id] = checker def handle_status_update(self, region_id, is_online): self.update_region_status(region_id, is_online) self.status_updated.emit(region_id, is_online) def currentData(self, role=Qt.ItemDataRole.UserRole + 1): return super().currentData(role) class SpotiFLACGUI(QWidget): def __init__(self): super().__init__() self.current_version = "4.6" self.tracks = [] self.all_tracks = [] self.successful_downloads = [] self.reset_state() self.settings = QSettings('SpotiFLAC', 'Settings') self.last_output_path = self.settings.value('output_path', str(Path.home() / "Music")) self.last_url = self.settings.value('spotify_url', '') self.filename_format = self.settings.value('filename_format', 'title_artist') self.use_track_numbers = self.settings.value('use_track_numbers', False, type=bool) self.use_artist_subfolders = self.settings.value('use_artist_subfolders', False, type=bool) self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool) self.service = self.settings.value('service', 'tidal') self.qobuz_region = self.settings.value('qobuz_region', 'us') self.qobuz_mode = self.settings.value('qobuz_mode', 'auto') self.check_for_updates = self.settings.value('check_for_updates', True, type=bool) self.current_theme_color = self.settings.value('theme_color', '#2196F3') self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration') self.date_format = self.settings.value('date_format', 'dd_mm_yyyy') self.elapsed_time = QTime(0, 0, 0) self.timer = QTimer(self) self.timer.timeout.connect(self.update_timer) self.network_manager = QNetworkAccessManager() self.network_manager.finished.connect(self.on_cover_loaded) self.initUI() if self.check_for_updates: QTimer.singleShot(0, self.check_updates) def set_combobox_value(self, combobox, target_value): for i in range(combobox.count()): if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value: combobox.setCurrentIndex(i) return True if combobox.itemData(i, Qt.ItemDataRole.UserRole) == target_value: combobox.setCurrentIndex(i) return True return False def check_updates(self): try: response = requests.get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/version.json") if response.status_code == 200: data = response.json() new_version = data.get("version") if new_version and version.parse(new_version) > version.parse(self.current_version): dialog = UpdateDialog(self.current_version, new_version, self) result = dialog.exec() if result == QDialog.DialogCode.Accepted: QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases")) except Exception as e: pass @staticmethod def format_duration(ms): minutes = ms // 60000 seconds = (ms % 60000) // 1000 return f"{minutes}:{seconds:02d}" def reset_state(self): self.tracks.clear() self.all_tracks.clear() self.is_album = False self.is_playlist = False self.is_single_track = False self.album_or_playlist_name = '' def reset_ui(self): self.track_list.clear() self.log_output.clear() self.progress_bar.setValue(0) self.progress_bar.hide() self.stop_btn.hide() self.pause_resume_btn.hide() self.pause_resume_btn.setText('Pause') self.reset_info_widget() self.hide_track_buttons() if hasattr(self, 'search_input'): self.search_input.clear() if hasattr(self, 'search_widget'): self.search_widget.hide() def initUI(self): self.setWindowTitle('SpotiFLAC') self.setFixedWidth(650) self.setMinimumHeight(350) icon_path = os.path.join(os.path.dirname(__file__), "icon.svg") if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) self.main_layout = QVBoxLayout() self.setup_spotify_section() self.setup_tabs() self.setLayout(self.main_layout) def setup_spotify_section(self): spotify_layout = QHBoxLayout() spotify_label = QLabel('Spotify URL:') spotify_label.setFixedWidth(100) self.spotify_url = QLineEdit() self.spotify_url.setPlaceholderText("Enter Spotify URL") self.spotify_url.setClearButtonEnabled(True) self.spotify_url.setText(self.last_url) self.spotify_url.textChanged.connect(self.save_url) self.fetch_btn = QPushButton('Fetch') self.fetch_btn.setFixedWidth(80) self.fetch_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.fetch_btn.clicked.connect(self.fetch_tracks) spotify_layout.addWidget(spotify_label) spotify_layout.addWidget(self.spotify_url) spotify_layout.addWidget(self.fetch_btn) self.main_layout.addLayout(spotify_layout) def filter_tracks(self): search_text = self.search_input.text().lower().strip() if not search_text: self.tracks = self.all_tracks.copy() else: self.tracks = [ track for track in self.all_tracks if (search_text in track.title.lower() or search_text in track.artists.lower() or search_text in track.album.lower()) ] self.update_track_list_display() def format_track_date(self, release_date): if not release_date: return "" try: if len(release_date) == 4: date_obj = datetime.strptime(release_date, "%Y") if self.date_format == "yyyy": return date_obj.strftime('%Y') else: return date_obj.strftime('%Y') elif len(release_date) == 7: date_obj = datetime.strptime(release_date, "%Y-%m") if self.date_format == "dd_mm_yyyy": return date_obj.strftime('%m-%Y') elif self.date_format == "yyyy_mm_dd": return date_obj.strftime('%Y-%m') else: return date_obj.strftime('%Y') else: date_obj = datetime.strptime(release_date, "%Y-%m-%d") if self.date_format == "dd_mm_yyyy": return date_obj.strftime('%d-%m-%Y') elif self.date_format == "yyyy_mm_dd": return date_obj.strftime('%Y-%m-%d') else: return date_obj.strftime('%Y') except ValueError: return release_date def update_track_list_display(self): self.track_list.clear() for i, track in enumerate(self.tracks, 1): duration = self.format_duration(track.duration_ms) formatted_date = self.format_track_date(track.release_date) if self.track_list_format == "artist_track_date_duration": display_parts = [f"{i}. {track.artists} - {track.title}"] if formatted_date: display_parts.append(formatted_date) display_parts.append(duration) display_text = " • ".join(display_parts) elif self.track_list_format == "track_artist_date": display_parts = [f"{i}. {track.title} - {track.artists}"] if formatted_date: display_parts.append(formatted_date) display_text = " • ".join(display_parts) elif self.track_list_format == "artist_track_date": display_parts = [f"{i}. {track.artists} - {track.title}"] if formatted_date: display_parts.append(formatted_date) display_text = " • ".join(display_parts) elif self.track_list_format == "track_artist_duration": display_text = f"{i}. {track.title} - {track.artists} • {duration}" elif self.track_list_format == "artist_track_duration": display_text = f"{i}. {track.artists} - {track.title} • {duration}" elif self.track_list_format == "track_artist": display_text = f"{i}. {track.title} - {track.artists}" elif self.track_list_format == "artist_track": display_text = f"{i}. {track.artists} - {track.title}" else: display_parts = [f"{i}. {track.title} - {track.artists}"] if formatted_date: display_parts.append(formatted_date) display_parts.append(duration) display_text = " • ".join(display_parts) self.track_list.addItem(display_text) def browse_output(self): directory = QFileDialog.getExistingDirectory(self, "Select Output Directory") if directory: self.output_dir.setText(directory) self.save_settings() def setup_tabs(self): self.tab_widget = QTabWidget() self.main_layout.addWidget(self.tab_widget) self.setup_dashboard_tab() self.setup_process_tab() self.setup_settings_tab() self.setup_theme_tab() self.setup_about_tab() def setup_dashboard_tab(self): dashboard_tab = QWidget() dashboard_layout = QVBoxLayout() self.setup_info_widget() dashboard_layout.addWidget(self.info_widget) self.track_list = QListWidget() self.track_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) dashboard_layout.addWidget(self.track_list) self.setup_track_buttons() dashboard_layout.addLayout(self.btn_layout) dashboard_layout.addWidget(self.single_track_container) dashboard_tab.setLayout(dashboard_layout) self.tab_widget.addTab(dashboard_tab, "Dashboard") self.hide_track_buttons() def setup_info_widget(self): self.info_widget = QWidget() info_layout = QHBoxLayout() self.cover_label = QLabel() self.cover_label.setFixedSize(80, 80) self.cover_label.setScaledContents(True) info_layout.addWidget(self.cover_label) text_info_layout = QVBoxLayout() self.title_label = QLabel() self.title_label.setStyleSheet("font-size: 14px; font-weight: bold;") self.title_label.setWordWrap(True) self.artists_label = QLabel() self.artists_label.setWordWrap(True) self.followers_label = QLabel() self.followers_label.setWordWrap(True) self.release_date_label = QLabel() self.release_date_label.setWordWrap(True) self.type_label = QLabel() self.type_label.setStyleSheet("font-size: 12px;") text_info_layout.addWidget(self.title_label) text_info_layout.addWidget(self.artists_label) text_info_layout.addWidget(self.followers_label) text_info_layout.addWidget(self.release_date_label) text_info_layout.addWidget(self.type_label) text_info_layout.addStretch() info_layout.addLayout(text_info_layout, 1) self.setup_search_widget() info_layout.addWidget(self.search_widget) self.info_widget.setLayout(info_layout) self.info_widget.setFixedHeight(100) self.info_widget.hide() def setup_search_widget(self): self.search_widget = QWidget() search_layout = QVBoxLayout() search_layout.setContentsMargins(10, 0, 0, 0) search_layout.addStretch() search_input_layout = QHBoxLayout() search_input_layout.addStretch() self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search...") self.search_input.setClearButtonEnabled(True) self.search_input.textChanged.connect(self.filter_tracks) self.search_input.setFixedWidth(250) search_input_layout.addWidget(self.search_input) search_layout.addLayout(search_input_layout) self.search_widget.setLayout(search_layout) self.search_widget.hide() def setup_track_buttons(self): self.btn_layout = QHBoxLayout() self.download_selected_btn = QPushButton('Download Selected') self.download_all_btn = QPushButton('Download All') self.remove_btn = QPushButton('Remove Selected') self.clear_btn = QPushButton('Clear') for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: btn.setMinimumWidth(120) btn.setCursor(Qt.CursorShape.PointingHandCursor) self.download_selected_btn.clicked.connect(self.download_selected) self.download_all_btn.clicked.connect(self.download_all) self.remove_btn.clicked.connect(self.remove_selected_tracks) self.clear_btn.clicked.connect(self.clear_tracks) self.btn_layout.addStretch() for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: self.btn_layout.addWidget(btn, 1) self.btn_layout.addStretch() self.single_track_container = QWidget() single_track_layout = QHBoxLayout(self.single_track_container) single_track_layout.setContentsMargins(0, 0, 0, 0) self.single_download_btn = QPushButton('Download') self.single_clear_btn = QPushButton('Clear') for btn in [self.single_download_btn, self.single_clear_btn]: btn.setFixedWidth(120) btn.setCursor(Qt.CursorShape.PointingHandCursor) self.single_download_btn.clicked.connect(self.download_all) self.single_clear_btn.clicked.connect(self.clear_tracks) single_track_layout.addStretch() single_track_layout.addWidget(self.single_download_btn) single_track_layout.addWidget(self.single_clear_btn) single_track_layout.addStretch() self.single_track_container.hide() def setup_process_tab(self): self.process_tab = QWidget() process_layout = QVBoxLayout() process_layout.setSpacing(5) self.log_output = QTextEdit() self.log_output.setReadOnly(True) process_layout.addWidget(self.log_output) progress_time_layout = QVBoxLayout() progress_time_layout.setSpacing(2) self.progress_bar = QProgressBar() progress_time_layout.addWidget(self.progress_bar) self.time_label = QLabel("00:00:00") self.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter) progress_time_layout.addWidget(self.time_label) process_layout.addLayout(progress_time_layout) control_layout = QHBoxLayout() self.stop_btn = QPushButton('Stop') self.pause_resume_btn = QPushButton('Pause') self.remove_successful_btn = QPushButton('Remove Finished Songs') self.stop_btn.setFixedWidth(120) self.pause_resume_btn.setFixedWidth(120) self.remove_successful_btn.setFixedWidth(200) self.stop_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.pause_resume_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.remove_successful_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.stop_btn.clicked.connect(self.stop_download) self.pause_resume_btn.clicked.connect(self.toggle_pause_resume) self.remove_successful_btn.clicked.connect(self.remove_successful_downloads) control_layout.addStretch() control_layout.addWidget(self.stop_btn) control_layout.addWidget(self.pause_resume_btn) control_layout.addWidget(self.remove_successful_btn) control_layout.addStretch() process_layout.addLayout(control_layout) self.process_tab.setLayout(process_layout) self.tab_widget.addTab(self.process_tab, "Process") self.progress_bar.hide() self.time_label.hide() self.stop_btn.hide() self.pause_resume_btn.hide() self.remove_successful_btn.hide() def setup_settings_tab(self): settings_tab = QWidget() settings_layout = QVBoxLayout() settings_layout.setSpacing(4) settings_layout.setContentsMargins(10, 10, 10, 10) output_group = QWidget() output_layout = QVBoxLayout(output_group) output_layout.setSpacing(2) output_layout.setContentsMargins(0, 0, 0, 0) output_label = QLabel('Output Directory') output_label.setStyleSheet("font-weight: bold; margin-top: 0px; margin-bottom: 5px;") output_layout.addWidget(output_label) output_dir_layout = QHBoxLayout() self.output_dir = QLineEdit() self.output_dir.setText(self.last_output_path) self.output_dir.textChanged.connect(self.save_settings) self.output_browse = QPushButton('Browse') self.output_browse.setFixedWidth(80) self.output_browse.setCursor(Qt.CursorShape.PointingHandCursor) self.output_browse.clicked.connect(self.browse_output) output_dir_layout.addWidget(self.output_dir) output_dir_layout.addSpacing(5) output_dir_layout.addWidget(self.output_browse) output_layout.addLayout(output_dir_layout) settings_layout.addWidget(output_group) dashboard_group = QWidget() dashboard_layout = QVBoxLayout(dashboard_group) dashboard_layout.setSpacing(3) dashboard_layout.setContentsMargins(0, 0, 0, 0) dashboard_label = QLabel('Dashboard Settings') dashboard_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") dashboard_layout.addWidget(dashboard_label) dashboard_controls_layout = QHBoxLayout() list_format_label = QLabel('Track List View:') list_format_label.setFixedWidth(90) self.track_list_format_dropdown = QComboBox() self.track_list_format_dropdown.addItem("Track - Artist - Date - Duration", "track_artist_date_duration") self.track_list_format_dropdown.addItem("Artist - Track - Date - Duration", "artist_track_date_duration") self.track_list_format_dropdown.addItem("Track - Artist - Date", "track_artist_date") self.track_list_format_dropdown.addItem("Artist - Track - Date", "artist_track_date") self.track_list_format_dropdown.addItem("Track - Artist - Duration", "track_artist_duration") self.track_list_format_dropdown.addItem("Artist - Track - Duration", "artist_track_duration") self.track_list_format_dropdown.addItem("Track - Artist", "track_artist") self.track_list_format_dropdown.addItem("Artist - Track", "artist_track") self.track_list_format_dropdown.currentIndexChanged.connect(self.save_track_list_format) dashboard_controls_layout.addWidget(list_format_label) dashboard_controls_layout.addWidget(self.track_list_format_dropdown) dashboard_controls_layout.addSpacing(15) date_format_label = QLabel('Date Format:') date_format_label.setFixedWidth(80) self.date_format_dropdown = QComboBox() self.date_format_dropdown.addItem("DD-MM-YYYY", "dd_mm_yyyy") self.date_format_dropdown.addItem("YYYY-MM-DD", "yyyy_mm_dd") self.date_format_dropdown.addItem("YYYY", "yyyy") self.date_format_dropdown.currentIndexChanged.connect(self.save_date_format) dashboard_controls_layout.addWidget(date_format_label) dashboard_controls_layout.addWidget(self.date_format_dropdown) dashboard_controls_layout.addStretch() dashboard_layout.addLayout(dashboard_controls_layout) settings_layout.addWidget(dashboard_group) file_group = QWidget() file_layout = QVBoxLayout(file_group) file_layout.setSpacing(2) file_layout.setContentsMargins(0, 0, 0, 0) file_label = QLabel('File Settings') file_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") file_layout.addWidget(file_label) format_layout = QHBoxLayout() format_label = QLabel('Filename Format:') self.format_group = QButtonGroup(self) self.title_artist_radio = QRadioButton('Title - Artist') self.title_artist_radio.setCursor(Qt.CursorShape.PointingHandCursor) self.title_artist_radio.toggled.connect(self.save_filename_format) self.artist_title_radio = QRadioButton('Artist - Title') self.artist_title_radio.setCursor(Qt.CursorShape.PointingHandCursor) self.artist_title_radio.toggled.connect(self.save_filename_format) self.title_only_radio = QRadioButton('Title') self.title_only_radio.setCursor(Qt.CursorShape.PointingHandCursor) self.title_only_radio.toggled.connect(self.save_filename_format) if hasattr(self, 'filename_format') and self.filename_format == "artist_title": self.artist_title_radio.setChecked(True) elif hasattr(self, 'filename_format') and self.filename_format == "title_only": self.title_only_radio.setChecked(True) else: self.title_artist_radio.setChecked(True) self.format_group.addButton(self.title_artist_radio) self.format_group.addButton(self.artist_title_radio) self.format_group.addButton(self.title_only_radio) format_layout.addWidget(format_label) format_layout.addWidget(self.title_artist_radio) format_layout.addSpacing(10) format_layout.addWidget(self.artist_title_radio) format_layout.addSpacing(10) format_layout.addWidget(self.title_only_radio) format_layout.addStretch() file_layout.addLayout(format_layout) checkbox_layout = QHBoxLayout() self.artist_subfolder_checkbox = QCheckBox('Artist Subfolder (Playlist)') self.artist_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.artist_subfolder_checkbox.setChecked(self.use_artist_subfolders) self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting) checkbox_layout.addWidget(self.artist_subfolder_checkbox) checkbox_layout.addSpacing(10) self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)') self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.album_subfolder_checkbox.setChecked(self.use_album_subfolders) self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting) checkbox_layout.addWidget(self.album_subfolder_checkbox) checkbox_layout.addSpacing(10) self.track_number_checkbox = QCheckBox('Track Number') self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.track_number_checkbox.setChecked(self.use_track_numbers) self.track_number_checkbox.toggled.connect(self.save_track_numbering) checkbox_layout.addWidget(self.track_number_checkbox) checkbox_layout.addStretch() file_layout.addLayout(checkbox_layout) settings_layout.addWidget(file_group) auth_group = QWidget() auth_layout = QVBoxLayout(auth_group) auth_layout.setSpacing(2) auth_layout.setContentsMargins(0, 0, 0, 0) auth_label = QLabel('Service Settings') auth_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") auth_layout.addWidget(auth_label) service_fallback_layout = QHBoxLayout() service_label = QLabel('Service:') self.service_dropdown = ServiceComboBox() self.service_dropdown.currentIndexChanged.connect(self.on_service_changed) service_fallback_layout.addWidget(service_label) service_fallback_layout.addWidget(self.service_dropdown) service_fallback_layout.addSpacing(10) self.qobuz_mode_label = QLabel('Mode:') self.qobuz_mode_dropdown = QComboBox() self.qobuz_mode_dropdown.addItem("Auto", "auto") self.qobuz_mode_dropdown.addItem("Region", "region") self.qobuz_mode_dropdown.currentIndexChanged.connect(self.on_qobuz_mode_changed) service_fallback_layout.addWidget(self.qobuz_mode_label) service_fallback_layout.addWidget(self.qobuz_mode_dropdown) service_fallback_layout.addSpacing(10) self.region_label = QLabel('Region:') self.qobuz_region_dropdown = QobuzRegionComboBox() self.qobuz_region_dropdown.currentIndexChanged.connect(self.save_qobuz_region_setting) service_fallback_layout.addWidget(self.region_label) service_fallback_layout.addWidget(self.qobuz_region_dropdown) self.qobuz_mode_label.hide() self.qobuz_mode_dropdown.hide() self.region_label.hide() self.qobuz_region_dropdown.hide() service_fallback_layout.addStretch() auth_layout.addLayout(service_fallback_layout) settings_layout.addWidget(auth_group) settings_layout.addStretch() settings_tab.setLayout(settings_layout) self.tab_widget.addTab(settings_tab, "Settings") self.set_combobox_value(self.service_dropdown, self.service) self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region) self.set_combobox_value(self.qobuz_mode_dropdown, self.qobuz_mode) self.set_combobox_value(self.track_list_format_dropdown, self.track_list_format) self.set_combobox_value(self.date_format_dropdown, self.date_format) self.update_service_ui() self.qobuz_region_dropdown.status_updated.connect( lambda region_id, is_online: self.service_dropdown.update_qobuz_status(region_id, is_online) ) def setup_theme_tab(self): theme_tab = QWidget() theme_layout = QVBoxLayout() theme_layout.setSpacing(8) theme_layout.setContentsMargins(8, 15, 15, 15) grid_layout = QVBoxLayout() self.color_buttons = {} first_row_palettes = [ ("Red", [ ("#FFCDD2", "100"), ("#EF9A9A", "200"), ("#E57373", "300"), ("#EF5350", "400"), ("#F44336", "500"), ("#E53935", "600"), ("#D32F2F", "700"), ("#C62828", "800"), ("#B71C1C", "900"), ("#FF8A80", "A100"), ("#FF5252", "A200"), ("#FF1744", "A400"), ("#D50000", "A700") ]), ("Pink", [ ("#F8BBD0", "100"), ("#F48FB1", "200"), ("#F06292", "300"), ("#EC407A", "400"), ("#E91E63", "500"), ("#D81B60", "600"), ("#C2185B", "700"), ("#AD1457", "800"), ("#880E4F", "900"), ("#FF80AB", "A100"), ("#FF4081", "A200"), ("#F50057", "A400"), ("#C51162", "A700") ]), ("Purple", [ ("#E1BEE7", "100"), ("#CE93D8", "200"), ("#BA68C8", "300"), ("#AB47BC", "400"), ("#9C27B0", "500"), ("#8E24AA", "600"), ("#7B1FA2", "700"), ("#6A1B9A", "800"), ("#4A148C", "900"), ("#EA80FC", "A100"), ("#E040FB", "A200"), ("#D500F9", "A400"), ("#AA00FF", "A700") ]) ] second_row_palettes = [ ("Deep Purple", [ ("#D1C4E9", "100"), ("#B39DDB", "200"), ("#9575CD", "300"), ("#7E57C2", "400"), ("#673AB7", "500"), ("#5E35B1", "600"), ("#512DA8", "700"), ("#4527A0", "800"), ("#311B92", "900"), ("#B388FF", "A100"), ("#7C4DFF", "A200"), ("#651FFF", "A400"), ("#6200EA", "A700") ]), ("Indigo", [ ("#C5CAE9", "100"), ("#9FA8DA", "200"), ("#7986CB", "300"), ("#5C6BC0", "400"), ("#3F51B5", "500"), ("#3949AB", "600"), ("#303F9F", "700"), ("#283593", "800"), ("#1A237E", "900"), ("#8C9EFF", "A100"), ("#536DFE", "A200"), ("#3D5AFE", "A400"), ("#304FFE", "A700") ]), ("Blue", [ ("#BBDEFB", "100"), ("#90CAF9", "200"), ("#64B5F6", "300"), ("#42A5F5", "400"), ("#2196F3", "500"), ("#1E88E5", "600"), ("#1976D2", "700"), ("#1565C0", "800"), ("#0D47A1", "900"), ("#82B1FF", "A100"), ("#448AFF", "A200"), ("#2979FF", "A400"), ("#2962FF", "A700") ]) ] third_row_palettes = [ ("Light Blue", [ ("#B3E5FC", "100"), ("#81D4FA", "200"), ("#4FC3F7", "300"), ("#29B6F6", "400"), ("#03A9F4", "500"), ("#039BE5", "600"), ("#0288D1", "700"), ("#0277BD", "800"), ("#01579B", "900"), ("#80D8FF", "A100"), ("#40C4FF", "A200"), ("#00B0FF", "A400"), ("#0091EA", "A700") ]), ("Cyan", [ ("#B2EBF2", "100"), ("#80DEEA", "200"), ("#4DD0E1", "300"), ("#26C6DA", "400"), ("#00BCD4", "500"), ("#00ACC1", "600"), ("#0097A7", "700"), ("#00838F", "800"), ("#006064", "900"), ("#84FFFF", "A100"), ("#18FFFF", "A200"), ("#00E5FF", "A400"), ("#00B8D4", "A700") ]), ("Teal", [ ("#B2DFDB", "100"), ("#80CBC4", "200"), ("#4DB6AC", "300"), ("#26A69A", "400"), ("#009688", "500"), ("#00897B", "600"), ("#00796B", "700"), ("#00695C", "800"), ("#004D40", "900"), ("#A7FFEB", "A100"), ("#64FFDA", "A200"), ("#1DE9B6", "A400"), ("#00BFA5", "A700") ]) ] fourth_row_palettes = [ ("Green", [ ("#C8E6C9", "100"), ("#A5D6A7", "200"), ("#81C784", "300"), ("#66BB6A", "400"), ("#4CAF50", "500"), ("#43A047", "600"), ("#388E3C", "700"), ("#2E7D32", "800"), ("#1B5E20", "900"), ("#B9F6CA", "A100"), ("#69F0AE", "A200"), ("#00E676", "A400"), ("#00C853", "A700") ]), ("Light Green", [ ("#DCEDC8", "100"), ("#C5E1A5", "200"), ("#AED581", "300"), ("#9CCC65", "400"), ("#8BC34A", "500"), ("#7CB342", "600"), ("#689F38", "700"), ("#558B2F", "800"), ("#33691E", "900"), ("#CCFF90", "A100"), ("#B2FF59", "A200"), ("#76FF03", "A400"), ("#64DD17", "A700") ]), ("Lime", [ ("#F0F4C3", "100"), ("#E6EE9C", "200"), ("#DCE775", "300"), ("#D4E157", "400"), ("#CDDC39", "500"), ("#C0CA33", "600"), ("#AFB42B", "700"), ("#9E9D24", "800"), ("#827717", "900"), ("#F4FF81", "A100"), ("#EEFF41", "A200"), ("#C6FF00", "A400"), ("#AEEA00", "A700") ]) ] fifth_row_palettes = [ ("Yellow", [ ("#FFF9C4", "100"), ("#FFF59D", "200"), ("#FFF176", "300"), ("#FFEE58", "400"), ("#FFEB3B", "500"), ("#FDD835", "600"), ("#FBC02D", "700"), ("#F9A825", "800"), ("#F57F17", "900"), ("#FFFF8D", "A100"), ("#FFFF00", "A200"), ("#FFEA00", "A400"), ("#FFD600", "A700") ]), ("Amber", [ ("#FFECB3", "100"), ("#FFE082", "200"), ("#FFD54F", "300"), ("#FFCA28", "400"), ("#FFC107", "500"), ("#FFB300", "600"), ("#FFA000", "700"), ("#FF8F00", "800"), ("#FF6F00", "900"), ("#FFE57F", "A100"), ("#FFD740", "A200"), ("#FFC400", "A400"), ("#FFAB00", "A700") ]), ("Orange", [ ("#FFE0B2", "100"), ("#FFCC80", "200"), ("#FFB74D", "300"), ("#FFA726", "400"), ("#FF9800", "500"), ("#FB8C00", "600"), ("#F57C00", "700"), ("#EF6C00", "800"), ("#E65100", "900"), ("#FFD180", "A100"), ("#FFAB40", "A200"), ("#FF9100", "A400"), ("#FF6D00", "A700") ]) ] for row_palettes in [first_row_palettes, second_row_palettes, third_row_palettes, fourth_row_palettes, fifth_row_palettes]: row_layout = QHBoxLayout() row_layout.setSpacing(15) for palette_name, colors in row_palettes: column_layout = QVBoxLayout() column_layout.setSpacing(3) palette_label = QLabel(palette_name) palette_label.setStyleSheet("margin-bottom: 2px;") palette_label.setAlignment(Qt.AlignmentFlag.AlignCenter) column_layout.addWidget(palette_label) color_buttons_layout = QHBoxLayout() color_buttons_layout.setSpacing(3) for color_hex, color_name in colors: color_btn = QPushButton() color_btn.setFixedSize(18, 18) is_current = color_hex == self.current_theme_color border_style = "2px solid #fff" if is_current else "none" color_btn.setStyleSheet(f""" QPushButton {{ background-color: {color_hex}; border: {border_style}; border-radius: 9px; }} QPushButton:hover {{ border: 2px solid #fff; }} QPushButton:pressed {{ border: 2px solid #fff; }} """) color_btn.setCursor(Qt.CursorShape.PointingHandCursor) color_btn.setToolTip(f"{palette_name} {color_name}\n{color_hex}") color_btn.clicked.connect(lambda checked, color=color_hex, btn=color_btn: self.change_theme_color(color, btn)) self.color_buttons[color_hex] = color_btn color_buttons_layout.addWidget(color_btn) column_layout.addLayout(color_buttons_layout) row_layout.addLayout(column_layout) grid_layout.addLayout(row_layout) theme_layout.addLayout(grid_layout) theme_layout.addStretch() theme_tab.setLayout(theme_layout) self.tab_widget.addTab(theme_tab, "Theme") def change_theme_color(self, color, clicked_btn=None): if hasattr(self, 'color_buttons'): for color_hex, btn in self.color_buttons.items(): if color_hex == self.current_theme_color: btn.setStyleSheet(f""" QPushButton {{ background-color: {color_hex}; border: none; border-radius: 9px; }} QPushButton:hover {{ border: 2px solid #fff; }} QPushButton:pressed {{ border: 2px solid #fff; }} """) break self.current_theme_color = color self.settings.setValue('theme_color', color) self.settings.sync() if clicked_btn: clicked_btn.setStyleSheet(f""" QPushButton {{ background-color: {color}; border: 2px solid #fff; border-radius: 9px; }} QPushButton:hover {{ border: 2px solid #fff; }} QPushButton:pressed {{ border: 2px solid #fff; }} """) qdarktheme.setup_theme( custom_colors={ "[dark]": { "primary": color, } } ) def setup_about_tab(self): about_tab = QWidget() about_layout = QVBoxLayout() about_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) about_layout.setSpacing(15) sections = [ ("Check for Updates", "Check", "https://github.com/afkarxyz/SpotiFLAC/releases"), ("Report an Issue", "Report", "https://github.com/afkarxyz/SpotiFLAC/issues") ] for title, button_text, url in sections: section_widget = QWidget() section_layout = QVBoxLayout(section_widget) section_layout.setSpacing(10) section_layout.setContentsMargins(0, 0, 0, 0) label = QLabel(title) label.setStyleSheet("color: palette(text); font-weight: bold;") label.setAlignment(Qt.AlignmentFlag.AlignCenter) section_layout.addWidget(label) button = QPushButton(button_text) button.setFixedSize(120, 25) button.setCursor(Qt.CursorShape.PointingHandCursor) button.clicked.connect(lambda _, url=url: QDesktopServices.openUrl(QUrl(url if url.startswith(('http://', 'https://')) else f'https://{url}'))) section_layout.addWidget(button, alignment=Qt.AlignmentFlag.AlignCenter) about_layout.addWidget(section_widget) footer_label = QLabel(f"v{self.current_version} | September 2025") about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter) about_tab.setLayout(about_layout) self.tab_widget.addTab(about_tab, "About") def on_service_changed(self, index): service = self.service_dropdown.currentData() self.service = service self.settings.setValue('service', service) self.settings.sync() self.update_service_ui() self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}") def on_qobuz_mode_changed(self, index): mode = self.qobuz_mode_dropdown.currentData() self.qobuz_mode = mode self.settings.setValue('qobuz_mode', mode) self.settings.sync() self.update_qobuz_mode_ui() self.log_output.append(f"Qobuz mode changed to: {self.qobuz_mode_dropdown.currentText()}") def update_service_ui(self): service = self.service if service == "qobuz": self.qobuz_mode_label.show() self.qobuz_mode_dropdown.show() self.update_qobuz_mode_ui() else: self.qobuz_mode_label.hide() self.qobuz_mode_dropdown.hide() self.region_label.hide() self.qobuz_region_dropdown.hide() def update_qobuz_mode_ui(self): mode = self.qobuz_mode_dropdown.currentData() if mode is None: mode = self.qobuz_mode if mode == "region": self.region_label.show() self.qobuz_region_dropdown.show() else: self.region_label.hide() self.qobuz_region_dropdown.hide() def save_url(self): self.settings.setValue('spotify_url', self.spotify_url.text().strip()) self.settings.sync() def save_filename_format(self): if self.artist_title_radio.isChecked(): self.filename_format = "artist_title" elif self.title_only_radio.isChecked(): self.filename_format = "title_only" else: self.filename_format = "title_artist" self.settings.setValue('filename_format', self.filename_format) self.settings.sync() def save_track_numbering(self): self.use_track_numbers = self.track_number_checkbox.isChecked() self.settings.setValue('use_track_numbers', self.use_track_numbers) self.settings.sync() def save_artist_subfolder_setting(self): self.use_artist_subfolders = self.artist_subfolder_checkbox.isChecked() self.settings.setValue('use_artist_subfolders', self.use_artist_subfolders) self.settings.sync() def save_album_subfolder_setting(self): self.use_album_subfolders = self.album_subfolder_checkbox.isChecked() self.settings.setValue('use_album_subfolders', self.use_album_subfolders) self.settings.sync() def save_qobuz_region_setting(self): region = self.qobuz_region_dropdown.currentData() self.qobuz_region = region self.settings.setValue('qobuz_region', region) self.settings.sync() self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}") def save_track_list_format(self): format_value = self.track_list_format_dropdown.currentData() self.track_list_format = format_value self.settings.setValue('track_list_format', format_value) self.settings.sync() if self.tracks: self.update_track_list_display() def save_date_format(self): format_value = self.date_format_dropdown.currentData() self.date_format = format_value self.settings.setValue('date_format', format_value) self.settings.sync() if self.tracks: self.update_track_list_display() def save_settings(self): self.settings.setValue('output_path', self.output_dir.text().strip()) self.settings.sync() self.log_output.append("Settings saved successfully!") def update_timer(self): self.elapsed_time = self.elapsed_time.addSecs(1) self.time_label.setText(self.elapsed_time.toString("hh:mm:ss")) def fetch_tracks(self): url = self.spotify_url.text().strip() if not url: self.log_output.append('Warning: Please enter a Spotify URL.') return try: self.reset_state() self.reset_ui() self.log_output.append('Just a moment. Fetching metadata...') self.tab_widget.setCurrentWidget(self.process_tab) self.metadata_worker = MetadataFetchWorker(url) self.metadata_worker.finished.connect(self.on_metadata_fetched) self.metadata_worker.error.connect(self.on_metadata_error) self.metadata_worker.start() except Exception as e: self.log_output.append(f'Error: Failed to start metadata fetch: {str(e)}') def on_metadata_fetched(self, metadata): try: url_info = parse_uri(self.spotify_url.text().strip()) if url_info["type"] == "track": self.handle_track_metadata(metadata["track"]) elif url_info["type"] == "album": self.handle_album_metadata(metadata) elif url_info["type"] == "playlist": self.handle_playlist_metadata(metadata) elif url_info["type"] == "artist_discography": self.handle_discography_metadata(metadata) elif url_info["type"] == "artist": self.handle_artist_metadata(metadata) self.update_button_states() self.tab_widget.setCurrentIndex(0) except Exception as e: self.log_output.append(f'Error: {str(e)}') def on_metadata_error(self, error_message): self.log_output.append(f'Error: {error_message}') def handle_track_metadata(self, track_data): track_id = track_data["external_urls"].split("/")[-1] track = Track( external_urls=track_data["external_urls"], title=track_data["name"], artists=track_data["artists"], album=track_data["album_name"], track_number=1, duration_ms=track_data.get("duration_ms", 0), id=track_id, isrc=track_data.get("isrc", ""), release_date=track_data.get("release_date", "") ) self.tracks = [track] self.all_tracks = [track] self.is_single_track = True self.is_album = self.is_playlist = False self.album_or_playlist_name = f"{self.tracks[0].title} - {self.tracks[0].artists}" metadata = { 'title': track_data["name"], 'artists': track_data["artists"], 'releaseDate': track_data["release_date"], 'cover': track_data["images"], 'duration_ms': track_data.get("duration_ms", 0) } self.update_display_after_fetch(metadata) def handle_album_metadata(self, album_data): self.album_or_playlist_name = album_data["album_info"]["name"] self.tracks = [] for track in album_data["track_list"]: track_id = track["external_urls"].split("/")[-1] self.tracks.append(Track( external_urls=track["external_urls"], title=track["name"], artists=track["artists"], album=self.album_or_playlist_name, track_number=track["track_number"], duration_ms=track.get("duration_ms", 0), id=track_id, isrc=track.get("isrc", ""), release_date=track.get("release_date", "") )) self.all_tracks = self.tracks.copy() self.is_album = True self.is_playlist = self.is_single_track = False metadata = { 'title': album_data["album_info"]["name"], 'artists': album_data["album_info"]["artists"], 'releaseDate': album_data["album_info"]["release_date"], 'cover': album_data["album_info"]["images"], 'total_tracks': album_data["album_info"]["total_tracks"] } self.update_display_after_fetch(metadata) def handle_playlist_metadata(self, playlist_data): self.album_or_playlist_name = playlist_data["playlist_info"]["owner"]["name"] self.tracks = [] for track in playlist_data["track_list"]: track_id = track["external_urls"].split("/")[-1] self.tracks.append(Track( external_urls=track["external_urls"], title=track["name"], artists=track["artists"], album=track["album_name"], track_number=track.get("track_number", len(self.tracks) + 1), duration_ms=track.get("duration_ms", 0), id=track_id, isrc=track.get("isrc", ""), release_date=track.get("release_date", "") )) self.all_tracks = self.tracks.copy() self.is_playlist = True self.is_album = self.is_single_track = False metadata = { 'title': playlist_data["playlist_info"]["owner"]["name"], 'artists': playlist_data["playlist_info"]["owner"]["display_name"], 'cover': playlist_data["playlist_info"]["owner"]["images"], 'followers': playlist_data["playlist_info"]["followers"]["total"], 'total_tracks': playlist_data["playlist_info"]["tracks"]["total"] } self.update_display_after_fetch(metadata) def handle_discography_metadata(self, discography_data): artist_info = discography_data["artist_info"] self.album_or_playlist_name = f"{artist_info['name']} - Discography ({artist_info['discography_type'].title()})" self.tracks = [] for track in discography_data["track_list"]: track_id = track["external_urls"].split("/")[-1] if track.get("external_urls") else "" self.tracks.append(Track( external_urls=track.get("external_urls", ""), title=track["name"], artists=track["artists"], album=track["album_name"], track_number=track.get("track_number", len(self.tracks) + 1), duration_ms=track.get("duration_ms", 0), id=track_id, isrc=track.get("isrc", ""), release_date=track.get("release_date", "") )) self.all_tracks = self.tracks.copy() self.is_playlist = True self.is_album = self.is_single_track = False metadata = { 'title': f"{artist_info['name']} - Discography", 'artists': f"{artist_info['discography_type'].title()} • {artist_info['total_albums']} albums", 'cover': artist_info["images"], 'followers': artist_info.get("followers", 0), 'total_tracks': len(self.tracks), 'discography_type': artist_info['discography_type'] } self.update_display_after_fetch(metadata) def handle_artist_metadata(self, artist_data): self.reset_state() metadata = { 'title': artist_data["artist"]["name"], 'artists': f"Followers: {artist_data['artist']['followers']:,}", 'cover': artist_data["artist"]["images"], 'followers': artist_data["artist"]["followers"], 'genres': artist_data["artist"].get("genres", []) } self.update_info_widget_artist_only(metadata) def update_display_after_fetch(self, metadata): self.track_list.setVisible(not self.is_single_track) if not self.is_single_track: self.search_widget.show() self.update_track_list_display() else: self.search_widget.hide() self.update_info_widget(metadata) def update_info_widget(self, metadata): self.title_label.setText(metadata['title']) if self.is_single_track or self.is_album: artists = metadata['artists'] if isinstance(metadata['artists'], list) else metadata['artists'].split(", ") label_text = "Artists" if len(artists) > 1 else "Artist" artists_text = ", ".join(artists) self.artists_label.setText(f"{label_text} {artists_text}") else: self.artists_label.setText(f"Owner {metadata['artists']}") if self.is_playlist and 'followers' in metadata: self.followers_label.setText(f"Followers {metadata['followers']:,}") self.followers_label.show() else: self.followers_label.hide() if metadata.get('releaseDate'): try: release_date = metadata['releaseDate'] if len(release_date) == 4: date_obj = datetime.strptime(release_date, "%Y") elif len(release_date) == 7: date_obj = datetime.strptime(release_date, "%Y-%m") else: date_obj = datetime.strptime(release_date, "%Y-%m-%d") formatted_date = date_obj.strftime("%d-%m-%Y") self.release_date_label.setText(f"Released {formatted_date}") self.release_date_label.show() except ValueError: self.release_date_label.setText(f"Released {metadata['releaseDate']}") self.release_date_label.show() else: self.release_date_label.hide() if self.is_single_track: duration = self.format_duration(metadata.get('duration_ms', 0)) self.type_label.setText(f"Duration {duration}") elif self.is_album: total_tracks = metadata.get('total_tracks', 0) self.type_label.setText(f"Album • {total_tracks} tracks") elif self.is_playlist: total_tracks = metadata.get('total_tracks', 0) if metadata.get('discography_type'): discography_type = metadata['discography_type'].title() self.type_label.setText(f"Discography ({discography_type}) • {total_tracks} tracks") else: self.type_label.setText(f"Playlist • {total_tracks} tracks") self.network_manager.get(QNetworkRequest(QUrl(metadata['cover']))) self.info_widget.show() def update_info_widget_artist_only(self, metadata): self.title_label.setText(metadata['title']) self.artists_label.setText(f"Followers {metadata['followers']:,}") if metadata.get('genres'): genres_text = ", ".join(metadata['genres'][:3]) if len(metadata['genres']) > 3: genres_text += f" (+{len(metadata['genres']) - 3} more)" self.followers_label.setText(f"Genres {genres_text}") self.followers_label.show() else: self.followers_label.hide() self.release_date_label.hide() self.type_label.setText("Artist Profile • No tracks available for download") self.network_manager.get(QNetworkRequest(QUrl(metadata['cover']))) self.track_list.hide() self.search_widget.hide() self.hide_track_buttons() self.info_widget.show() def reset_info_widget(self): self.title_label.clear() self.artists_label.clear() self.followers_label.clear() self.release_date_label.clear() self.type_label.clear() self.cover_label.clear() self.info_widget.hide() def on_cover_loaded(self, reply): if reply.error() == QNetworkReply.NetworkError.NoError: data = reply.readAll() pixmap = QPixmap() pixmap.loadFromData(data) self.cover_label.setPixmap(pixmap) def update_button_states(self): if self.is_single_track: for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: btn.hide() self.single_track_container.show() self.single_download_btn.setEnabled(True) self.single_clear_btn.setEnabled(True) else: self.single_track_container.hide() self.download_selected_btn.show() self.download_all_btn.show() self.remove_btn.show() self.clear_btn.show() self.download_all_btn.setText('Download All') self.clear_btn.setText('Clear') self.download_all_btn.setMinimumWidth(120) self.clear_btn.setMinimumWidth(120) self.download_selected_btn.setEnabled(True) self.download_all_btn.setEnabled(True) def hide_track_buttons(self): buttons = [ self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn ] for btn in buttons: btn.hide() if hasattr(self, 'single_track_container'): self.single_track_container.hide() def download_selected(self): if self.is_single_track: self.download_all() else: selected_items = self.track_list.selectedItems() if not selected_items: self.log_output.append('Warning: Please select tracks to download.') return selected_indices = [self.track_list.row(item) for item in selected_items] self.download_tracks(selected_indices) def download_all(self): if self.is_single_track: self.download_tracks([0]) else: self.download_tracks(range(len(self.tracks))) def download_tracks(self, indices): self.log_output.clear() raw_outpath = self.output_dir.text().strip() outpath = os.path.normpath(raw_outpath) if not os.path.exists(outpath): self.log_output.append('Warning: Invalid output directory.') return tracks_to_download = self.tracks if self.is_single_track else [self.tracks[i] for i in indices] if self.is_album or self.is_playlist: name = self.album_or_playlist_name.strip() folder_name = re.sub(r'[<>:"/\\|?*]', '_', name) outpath = os.path.join(outpath, folder_name) os.makedirs(outpath, exist_ok=True) try: self.start_download_worker(tracks_to_download, outpath) except Exception as e: self.log_output.append(f"Error: An error occurred while starting the download: {str(e)}") def start_download_worker(self, tracks_to_download, outpath): service = self.service_dropdown.currentData() qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us" qobuz_mode = self.qobuz_mode_dropdown.currentData() if service == "qobuz" else "auto" self.worker = DownloadWorker( tracks_to_download, outpath, self.is_single_track, self.is_album, self.is_playlist, self.album_or_playlist_name, self.filename_format, self.use_track_numbers, self.use_artist_subfolders, self.use_album_subfolders, service, qobuz_region, qobuz_mode ) self.worker.finished.connect(lambda success, message, failed_tracks, successful_tracks, skipped_tracks: self.on_download_finished(success, message, failed_tracks, successful_tracks, skipped_tracks)) self.worker.progress.connect(self.update_progress) self.worker.start() self.start_timer() self.update_ui_for_download_start() def update_ui_for_download_start(self): self.download_selected_btn.setEnabled(False) self.download_all_btn.setEnabled(False) if hasattr(self, 'single_download_btn'): self.single_download_btn.setEnabled(False) if hasattr(self, 'single_clear_btn'): self.single_clear_btn.setEnabled(False) self.stop_btn.show() self.pause_resume_btn.show() self.progress_bar.show() self.progress_bar.setValue(0) self.tab_widget.setCurrentWidget(self.process_tab) def update_progress(self, message, percentage): self.log_output.append(message) self.log_output.moveCursor(QTextCursor.MoveOperation.End) if percentage > 0: self.progress_bar.setValue(percentage) def stop_download(self): if hasattr(self, 'worker'): self.worker.stop() self.stop_timer() self.on_download_finished(True, "Download stopped by user.", [], [], []) def on_download_finished(self, success, message, failed_tracks, successful_tracks=None, skipped_tracks=None): self.progress_bar.hide() self.stop_btn.hide() self.pause_resume_btn.hide() self.pause_resume_btn.setText('Pause') self.stop_timer() if successful_tracks is not None: self.successful_downloads = successful_tracks if skipped_tracks is not None: self.skipped_downloads = skipped_tracks if (hasattr(self, 'successful_downloads') and self.successful_downloads) or (hasattr(self, 'skipped_downloads') and self.skipped_downloads): self.remove_successful_btn.show() else: self.remove_successful_btn.hide() self.download_selected_btn.setEnabled(True) self.download_all_btn.setEnabled(True) if hasattr(self, 'single_download_btn'): self.single_download_btn.setEnabled(True) if hasattr(self, 'single_clear_btn'): self.single_clear_btn.setEnabled(True) if success: self.log_output.append(f"\nStatus: {message}") if failed_tracks: self.log_output.append("\nFailed downloads:") for title, artists, error in failed_tracks: self.log_output.append(f"• {title} - {artists}") self.log_output.append(f" Error: {error}\n") else: self.log_output.append(f"Error: {message}") self.tab_widget.setCurrentWidget(self.process_tab) def toggle_pause_resume(self): if hasattr(self, 'worker'): if self.worker.is_paused: self.worker.resume() self.pause_resume_btn.setText('Pause') self.timer.start(1000) else: self.worker.pause() self.pause_resume_btn.setText('Resume') def remove_successful_downloads(self): """Remove successfully downloaded and skipped tracks from the dashboard""" successful_tracks = getattr(self, 'successful_downloads', []) skipped_tracks = getattr(self, 'skipped_downloads', []) if not successful_tracks and not skipped_tracks: self.log_output.append("No downloaded or skipped tracks to remove.") return tracks_to_remove = [] # Check for successful downloads for track in self.tracks: for successful_track in successful_tracks: if (track.title == successful_track.title and track.artists == successful_track.artists and track.album == successful_track.album): tracks_to_remove.append(track) break # Check for skipped tracks (already exists) for track in self.tracks: for skipped_track in skipped_tracks: if (track.title == skipped_track.title and track.artists == skipped_track.artists and track.album == skipped_track.album): if track not in tracks_to_remove: # Avoid duplicates tracks_to_remove.append(track) break if tracks_to_remove: for track in tracks_to_remove: if track in self.tracks: self.tracks.remove(track) if track in self.all_tracks: self.all_tracks.remove(track) self.update_track_list_display() successful_count = len([t for t in tracks_to_remove if t in successful_tracks]) skipped_count = len([t for t in tracks_to_remove if t in skipped_tracks]) message = f"Removed {len(tracks_to_remove)} tracks from the list" if successful_count > 0: message += f" ({successful_count} downloaded" if skipped_count > 0: message += f", {skipped_count} already existed" if successful_count > 0 else f" ({skipped_count} already existed" if successful_count > 0 or skipped_count > 0: message += ")" self.log_output.append(message + ".") self.tab_widget.setCurrentIndex(0) else: self.log_output.append("No matching tracks found in the current list.") self.remove_successful_btn.hide() def remove_selected_tracks(self): if not self.is_single_track: selected_items = self.track_list.selectedItems() selected_indices = [self.track_list.row(item) for item in selected_items] tracks_to_remove = [self.tracks[i] for i in selected_indices] for track in tracks_to_remove: if track in self.tracks: self.tracks.remove(track) if track in self.all_tracks: self.all_tracks.remove(track) self.update_track_list_display() def clear_tracks(self): self.reset_state() self.reset_ui() self.tab_widget.setCurrentIndex(0) def start_timer(self): self.elapsed_time = QTime(0, 0, 0) self.time_label.setText("00:00:00") self.time_label.show() self.timer.start(1000) def stop_timer(self): self.timer.stop() self.time_label.hide() def closeEvent(self, event): if hasattr(self, 'timer'): self.timer.stop() if hasattr(self, 'service_dropdown'): for attr_name in ['tidal_status_checker', 'deezer_status_checker']: if hasattr(self.service_dropdown, attr_name): checker = getattr(self.service_dropdown, attr_name) if checker.isRunning(): checker.quit() checker.wait() if hasattr(self, 'qobuz_region_dropdown'): for checker in self.qobuz_region_dropdown.status_checkers.values(): if checker.isRunning(): checker.quit() checker.wait() if hasattr(self, 'worker') and self.worker and self.worker.isRunning(): self.worker.stop() self.worker.quit() self.worker.wait() event.accept() if __name__ == '__main__': try: if sys.platform == "win32": import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') except Exception as e: pass app = QApplication(sys.argv) settings = QSettings('SpotiFLAC', 'Settings') theme_color = settings.value('theme_color', '#2196F3') qdarktheme.setup_theme( custom_colors={ "[dark]": { "primary": theme_color, } } ) ex = SpotiFLACGUI() ex.show() sys.exit(app.exec())