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)
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 = []
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))
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
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))
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))
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"
self.finished.emit(True, success_message, self.failed_tracks)
except Exception as e:
self.finished.emit(False, str(e), self.failed_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.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.stop_btn.setFixedWidth(120)
self.pause_resume_btn.setFixedWidth(120)
self.stop_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.pause_resume_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.stop_btn.clicked.connect(self.stop_download)
self.pause_resume_btn.clicked.connect(self.toggle_pause_resume)
control_layout.addStretch()
control_layout.addWidget(self.stop_btn)
control_layout.addWidget(self.pause_resume_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()
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(self.on_download_finished)
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):
self.progress_bar.hide()
self.stop_btn.hide()
self.pause_resume_btn.hide()
self.pause_resume_btn.setText('Pause')
self.stop_timer()
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_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())