diff --git a/README.md b/README.md
index 7acb4e3..a59d2d4 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,15 @@

-SpotiFLAC allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music, and Deezer (via Lucida), as well as Qobuz (via SquidWTF).
+SpotiFLAC allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Deezer with the help of Lucida.
-### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v2.9/SpotiFLAC.exe)
+### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v2.6/SpotiFLAC.exe)
#
-> [!Note]
-**Download speed** from Lucida is unpredictable—sometimes fast, sometimes slow. Join their [Discord](https://discord.com/invite/dXEGRWqEbS) for updates.
+> [!WARNING]
+Sometimes, the **download speed** from Lucida can be fast or slow; it varies unpredictably.
## Screenshots
@@ -21,9 +21,11 @@

+
+

-
+
> When **Fallback** is enabled, it will use the backup server `Lucida.su`
diff --git a/SpotiFLAC.py b/SpotiFLAC.py
index 7e64ae8..4fc3337 100644
--- a/SpotiFLAC.py
+++ b/SpotiFLAC.py
@@ -5,22 +5,19 @@ from datetime import datetime
import requests
import re
from packaging import version
-import json
-import asyncio
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit,
QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton,
QAbstractItemView, QSpacerItem, QSizePolicy, QProgressBar, QCheckBox, QDialog,
- QDialogButtonBox, QComboBox, QStyledItemDelegate
+ QDialogButtonBox, QComboBox, QStyledItemDelegate, QStyle
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize
-from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush
+from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush, QPalette
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException, get_raw_spotify_data
+from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException
from getTracks import TrackDownloader
-import SquidWTF
@dataclass
class Track:
@@ -58,8 +55,7 @@ class DownloadWorker(QThread):
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_album_subfolders=False, use_fallback=False, service="amazon", timeout=30,
- qobuz_region="us"):
+ use_album_subfolders=False, use_fallback=False, service="amazon", timeout=30):
super().__init__()
self.tracks = tracks
self.outpath = outpath
@@ -73,7 +69,6 @@ class DownloadWorker(QThread):
self.use_fallback = use_fallback
self.service = service
self.timeout = timeout
- self.qobuz_region = qobuz_region
self.is_paused = False
self.is_stopped = False
self.failed_tracks = []
@@ -89,15 +84,17 @@ class DownloadWorker(QThread):
try:
downloader = TrackDownloader(self.use_fallback, self.timeout)
- def progress_update_lucida(current, total, current_overall_progress):
+ def progress_update(current, total):
if total > 0:
percent = (current / total) * 100
current_mb = current / (1024 * 1024)
total_mb = total / (1024 * 1024)
self.progress.emit(f"Download progress: {percent:.2f}% ({current_mb:.2f}MB/{total_mb:.2f}MB)",
- current_overall_progress)
+ int(percent))
else:
- self.progress.emit(f"Processing metadata...", current_overall_progress)
+ self.progress.emit(f"Processing metadata...", 0)
+
+ downloader.set_progress_callback(progress_update)
total_tracks = len(self.tracks)
@@ -109,15 +106,12 @@ class DownloadWorker(QThread):
if self.is_stopped:
return
- current_overall_progress = int((i / total_tracks) * 100)
- next_overall_progress = int(((i + 1) / total_tracks) * 100)
-
self.progress.emit(f"Starting download ({i+1}/{total_tracks}): {track.title} - {track.artists}",
- current_overall_progress)
+ int((i) / total_tracks * 100))
try:
track_id = track.id
- self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", current_overall_progress)
+ self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0)
if self.is_playlist and self.use_album_subfolders:
album_folder = re.sub(r'[<>:"/\\|?*]', '_', track.album)
@@ -126,133 +120,41 @@ class DownloadWorker(QThread):
else:
track_outpath = self.outpath
- if self.service == "qobuz":
- self.progress.emit(f"Getting track metadata for: {track.title} - {track.artists}", current_overall_progress)
-
- isrc = None
- try:
- track_url = track.external_urls
- self.progress.emit(f"Fetching Spotify metadata for ISRC...", current_overall_progress)
- raw_data = get_raw_spotify_data(track_url)
- if raw_data and "external_ids" in raw_data and "isrc" in raw_data["external_ids"]:
- isrc = raw_data["external_ids"]["isrc"]
- self.progress.emit(f"Found ISRC from Spotify: {isrc}", current_overall_progress)
- except Exception as e:
- self.progress.emit(f"Could not get ISRC from Spotify raw data: {str(e)}", current_overall_progress)
-
- if not isrc:
- self.progress.emit(f"No ISRC found, searching by title and artist", current_overall_progress)
- search_query = f"{track.title} {track.artists}"
- try:
- self.progress.emit(f"Searching Qobuz for: {search_query}", current_overall_progress)
- qobuz_track_info = SquidWTF.search_track(track.title, track.artists, strict_match=True, region=self.qobuz_region)
- if qobuz_track_info:
- qobuz_track_id = qobuz_track_info["id"]
- self.progress.emit(f"Found track on Qobuz by title search: {qobuz_track_info['title']} - {qobuz_track_info['performer']['name']}", current_overall_progress)
- found_artist = qobuz_track_info['performer']['name'].lower()
- expected_artist = track.artists.lower()
- if expected_artist not in found_artist and found_artist not in expected_artist:
- self.progress.emit(f"Warning: Artist mismatch! Expected: {track.artists}, Found: {qobuz_track_info['performer']['name']}", current_overall_progress)
- raise Exception(f"Artist mismatch: Expected '{track.artists}', found '{qobuz_track_info['performer']['name']}'")
- else:
- raise Exception(f"Could not find track on Qobuz: {track.title} - {track.artists}")
- except Exception as e:
- self.progress.emit(f"Search by title failed: {str(e)}", current_overall_progress)
- raise Exception(f"Could not find track on Qobuz: {track.title} - {track.artists}")
- else:
- self.progress.emit(f"Searching Qobuz with ISRC: {isrc}", current_overall_progress)
- qobuz_track_info = SquidWTF.get_track_info(isrc, region=self.qobuz_region)
- qobuz_track_id = qobuz_track_info["id"]
- self.progress.emit(f"Found track on Qobuz: {qobuz_track_info['title']} - {qobuz_track_info['performer']['name']}", current_overall_progress)
- found_artist = qobuz_track_info['performer']['name'].lower()
- expected_artist = track.artists.lower()
- if expected_artist not in found_artist and found_artist not in expected_artist:
- self.progress.emit(f"Warning: Artist mismatch! Expected: {track.artists}, Found: {qobuz_track_info['performer']['name']}", current_overall_progress)
-
- download_url = SquidWTF.get_download_url(qobuz_track_id, region=self.qobuz_region)
- os.makedirs(track_outpath, exist_ok=True)
- temp_filename = os.path.join(track_outpath, f"temp_{qobuz_track_id}.flac")
- self.progress.emit(f"Downloading from Qobuz...", current_overall_progress)
-
- def progress_callback_qobuz(current, total):
- if total > 0:
- percent = (current / total) * 100
- current_mb = current / (1024 * 1024)
- total_mb = total / (1024 * 1024)
- self.progress.emit(f"Download progress: {percent:.2f}% ({current_mb:.2f}MB/{total_mb:.2f}MB)",
- current_overall_progress)
-
- try:
- SquidWTF.download_file(download_url, temp_filename, progress_callback_qobuz)
-
- if not os.path.exists(temp_filename) or os.path.getsize(temp_filename) == 0:
- raise Exception(f"Downloaded file is empty or does not exist: {temp_filename}")
-
- self.progress.emit(f"Embedding metadata...", current_overall_progress)
- SquidWTF.embed_metadata(temp_filename, qobuz_track_info)
-
- if (self.is_album or (self.is_playlist and self.use_album_subfolders)) 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'[<>:"/\\|?*]', '_', new_filename)
- new_filepath = os.path.join(track_outpath, new_filename)
-
- if os.path.exists(new_filepath):
- os.remove(new_filepath)
- os.rename(temp_filename, new_filepath)
-
- downloaded_file = new_filepath
- self.progress.emit(f"File renamed to: {new_filename}", current_overall_progress)
- except Exception as e:
- self.progress.emit(f"Error during download or processing: {str(e)}", current_overall_progress)
- if os.path.exists(temp_filename):
- try:
- os.remove(temp_filename)
- self.progress.emit(f"Removed incomplete download file", current_overall_progress)
- except:
- pass
- raise Exception(f"Failed to download or process file: {str(e)}")
+ import asyncio
+ metadata = asyncio.run(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_album or (self.is_playlist and self.use_album_subfolders)) and self.use_track_numbers:
+ new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}"
else:
- metadata = asyncio.run(downloader.get_track_info(track_id, self.service))
-
- self.progress.emit(f"Track info received, starting download process", current_overall_progress)
-
- is_paused_callback = lambda: self.is_paused
- is_stopped_callback = lambda: self.is_stopped
-
- downloader.set_progress_callback(
- lambda current, total: progress_update_lucida(current, total, current_overall_progress)
- )
-
- downloaded_file = downloader.download(
- metadata,
- track_outpath,
- is_paused_callback=is_paused_callback,
- is_stopped_callback=is_stopped_callback
- )
-
- if (self.is_album or (self.is_playlist and self.use_album_subfolders)) 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'[<>:"/\\|?*]', '_', new_filename)
- new_filepath = os.path.join(track_outpath, new_filename)
-
- if os.path.exists(downloaded_file) and downloaded_file != new_filepath:
- if os.path.exists(new_filepath):
- os.remove(new_filepath)
- os.rename(downloaded_file, new_filepath)
- self.progress.emit(f"File renamed to: {new_filename}", current_overall_progress)
+ new_filename = self.get_formatted_filename(track)
+
+ new_filename = re.sub(r'[<>:"/\\|?*]', '_', new_filename)
+ new_filepath = os.path.join(track_outpath, new_filename)
+
+ if os.path.exists(downloaded_file) and downloaded_file != new_filepath:
+ if os.path.exists(new_filepath):
+ os.remove(new_filepath)
+ os.rename(downloaded_file, new_filepath)
+ self.progress.emit(f"File renamed to: {new_filename}", 0)
self.progress.emit(f"Successfully downloaded: {track.title} - {track.artists}",
- next_overall_progress)
+ 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)}",
- next_overall_progress)
+ int((i + 1) / total_tracks * 100))
continue
if not self.is_stopped:
@@ -316,51 +218,23 @@ class ServiceStatusChecker(QThread):
error = pyqtSignal(str)
def run(self):
- services_status = {
- 'amazon': False,
- 'tidal': False,
- 'deezer': False,
- 'qobuz': False
- }
-
try:
response = requests.get("https://lucida.to/api/stats", timeout=5)
if response.status_code == 200:
- try:
- data = response.json()
- current_services = data.get('all', {}).get('downloads', {}).get('current', {}).get('services', {})
-
- services_status['amazon'] = current_services.get('amazon', 0) > 0
- services_status['tidal'] = current_services.get('tidal', 0) > 0
- services_status['deezer'] = current_services.get('deezer', 0) > 0
-
- except json.JSONDecodeError:
- pass
- except Exception:
- pass
- except requests.exceptions.RequestException:
- pass
- except Exception:
- pass
-
- eu_online = False
- us_online = False
-
- try:
- eu_response = requests.get("https://eu.qobuz.squid.wtf", timeout=5)
- eu_online = eu_response.status_code in [200, 304]
- except Exception:
- pass
-
- try:
- us_response = requests.get("https://us.qobuz.squid.wtf", timeout=5)
- us_online = us_response.status_code in [200, 304]
- except Exception:
- pass
-
- services_status['qobuz'] = eu_online or us_online
-
- self.status_updated.emit(services_status)
+ data = response.json()
+ services_status = {}
+
+ current_services = data.get('all', {}).get('downloads', {}).get('current', {}).get('services', {})
+
+ services_status['amazon'] = current_services.get('amazon', 0) > 0
+ services_status['tidal'] = current_services.get('tidal', 0) > 0
+ services_status['deezer'] = current_services.get('deezer', 0) > 0
+
+ self.status_updated.emit(services_status)
+ else:
+ self.error.emit(f"Server returned status code: {response.status_code}")
+ except Exception as e:
+ self.error.emit(f"Error checking service status: {str(e)}")
class StatusIndicatorDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
@@ -369,9 +243,11 @@ class StatusIndicatorDelegate(QStyledItemDelegate):
super().paint(painter, option, index)
- if item_data and item_data.get('online') is None:
- return
-
+ if option.state & QStyle.StateFlag.State_Selected:
+ text_color = option.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText)
+ else:
+ text_color = option.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.Text)
+
indicator_color = Qt.GlobalColor.green if is_online else Qt.GlobalColor.red
circle_size = 6
@@ -409,8 +285,7 @@ class ServiceComboBox(QComboBox):
self.services = [
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False},
{'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png', 'online': False},
- {'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False},
- {'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False}
+ {'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False}
]
for service in self.services:
@@ -453,90 +328,10 @@ class ServiceComboBox(QComboBox):
def currentData(self, role=Qt.ItemDataRole.UserRole + 1):
return super().currentData(role)
-class QobuzRegionComboBox(QComboBox):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setIconSize(QSize(16, 16))
- self.regions_status = {}
-
- self.setItemDelegate(StatusIndicatorDelegate())
-
- self.setup_items()
-
- self.status_checker = QThread()
- self.status_checker.run = self.check_status
- self.status_checker.finished.connect(self.update_region_status)
- self.status_checker.start()
-
- self.status_timer = QTimer(self)
- self.status_timer.timeout.connect(self.refresh_status)
- self.status_timer.start(5000)
-
- def setup_items(self):
- current_dir = os.path.dirname(os.path.abspath(__file__))
-
- self.regions = [
- {'id': 'eu', 'name': 'Europe', 'icon': 'eu.svg', 'online': False, 'url': 'https://eu.qobuz.squid.wtf'},
- {'id': 'us', 'name': 'North America', 'icon': 'us.svg', 'online': False, 'url': 'https://us.qobuz.squid.wtf'}
- ]
-
- 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 check_status(self):
- regions_status = {}
-
- for region in self.regions:
- try:
- response = requests.get(region['url'], timeout=5)
- regions_status[region['id']] = response.status_code in [200, 304]
- except requests.exceptions.RequestException:
- regions_status[region['id']] = False
- except Exception:
- regions_status[region['id']] = False
-
- self.regions_status = regions_status
-
- def update_region_status(self):
- for i in range(self.count()):
- region_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1)
-
- if region_id in self.regions_status:
- region_data = self.itemData(i, Qt.ItemDataRole.UserRole)
- if isinstance(region_data, dict):
- region_data['online'] = self.regions_status[region_id]
- self.setItemData(i, region_data, Qt.ItemDataRole.UserRole)
-
- self.update()
-
- def refresh_status(self):
- self.status_checker = QThread()
- self.status_checker.run = self.check_status
- self.status_checker.finished.connect(self.update_region_status)
- self.status_checker.start()
-
- def currentData(self, role=Qt.ItemDataRole.UserRole + 1):
- return super().currentData(role)
-
class SpotiFLACGUI(QWidget):
def __init__(self):
super().__init__()
- self.current_version = "2.9"
+ self.current_version = "2.6"
self.tracks = []
self.reset_state()
@@ -549,7 +344,6 @@ class SpotiFLACGUI(QWidget):
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
self.use_fallback = self.settings.value('use_fallback', False, type=bool)
self.service = self.settings.value('service', 'amazon')
- self.qobuz_region = self.settings.value('qobuz_region', 'us')
self.timeout_value = self.settings.value('timeout_value', 30, type=int)
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
@@ -583,8 +377,8 @@ class SpotiFLACGUI(QWidget):
if result == QDialog.DialogCode.Accepted:
QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases"))
- except Exception:
- pass
+ except Exception as e:
+ print(f"Error checking for updates: {e}")
@staticmethod
def format_duration(ms):
@@ -625,7 +419,6 @@ class SpotiFLACGUI(QWidget):
self.setup_tabs()
self.setLayout(self.main_layout)
- QTimer.singleShot(0, self.update_service_ui_visibility)
def setup_spotify_section(self):
spotify_layout = QHBoxLayout()
@@ -870,83 +663,47 @@ class SpotiFLACGUI(QWidget):
auth_layout = QVBoxLayout(auth_group)
auth_layout.setSpacing(5)
- auth_label = QLabel('Service Setting')
+ auth_label = QLabel('Lucida Settings')
auth_label.setStyleSheet("font-weight: bold;")
auth_layout.addWidget(auth_label)
- source_fallback_layout = QHBoxLayout()
+ service_fallback_layout = QHBoxLayout()
- service_label = QLabel('Source:')
+ service_label = QLabel('Service:')
self.service_dropdown = ServiceComboBox()
self.service_dropdown.currentIndexChanged.connect(self.save_service_setting)
- saved_service = self.service
- for i in range(self.service_dropdown.count()):
- if self.service_dropdown.itemData(i, Qt.ItemDataRole.UserRole + 1) == saved_service:
- self.service_dropdown.setCurrentIndex(i)
- break
+ service_fallback_layout.addWidget(service_label)
+ service_fallback_layout.addWidget(self.service_dropdown)
- source_fallback_layout.addWidget(service_label)
- source_fallback_layout.addWidget(self.service_dropdown)
-
- self.qobuz_region_dropdown = QobuzRegionComboBox()
- self.qobuz_region_dropdown.setFixedWidth(150)
- self.qobuz_region_dropdown.currentIndexChanged.connect(self.save_qobuz_region_setting)
- self.qobuz_region_dropdown.setVisible(False)
- source_fallback_layout.addWidget(self.qobuz_region_dropdown)
-
- saved_region = self.qobuz_region
- for i in range(self.qobuz_region_dropdown.count()):
- if self.qobuz_region_dropdown.itemData(i, Qt.ItemDataRole.UserRole + 1) == saved_region:
- self.qobuz_region_dropdown.setCurrentIndex(i)
- break
-
- source_fallback_layout.addSpacing(20)
+ service_fallback_layout.addSpacing(20)
self.fallback_checkbox = QCheckBox('Fallback')
self.fallback_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.fallback_checkbox.setChecked(self.use_fallback)
self.fallback_checkbox.toggled.connect(self.save_fallback_setting)
- source_fallback_layout.addWidget(self.fallback_checkbox)
+ service_fallback_layout.addWidget(self.fallback_checkbox)
- source_fallback_layout.addSpacing(20)
+ service_fallback_layout.addSpacing(20)
timeout_label = QLabel('Timeout:')
- self.timeout_label = timeout_label
self.timeout_input = QLineEdit()
self.timeout_input.setText(str(self.timeout_value))
self.timeout_input.setFixedWidth(60)
self.timeout_input.textChanged.connect(self.save_timeout_setting)
- source_fallback_layout.addWidget(timeout_label)
- source_fallback_layout.addWidget(self.timeout_input)
-
- source_fallback_layout.addStretch()
- auth_layout.addLayout(source_fallback_layout)
+ service_fallback_layout.addWidget(timeout_label)
+ service_fallback_layout.addWidget(self.timeout_input)
+
+ 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")
-
- def update_service_ui_visibility(self):
- if hasattr(self, 'fallback_checkbox') and hasattr(self, 'timeout_input') and hasattr(self, 'timeout_label'):
- service = self.service_dropdown.currentData() if hasattr(self, 'service_dropdown') else self.service
-
- if service == "qobuz":
- self.fallback_checkbox.setVisible(False)
- self.timeout_input.setVisible(False)
- self.timeout_label.setVisible(False)
- if hasattr(self, 'qobuz_region_dropdown'):
- self.qobuz_region_dropdown.setVisible(True)
- else:
- self.fallback_checkbox.setVisible(True)
- self.timeout_input.setVisible(True)
- self.timeout_label.setVisible(True)
- if hasattr(self, 'qobuz_region_dropdown'):
- self.qobuz_region_dropdown.setVisible(False)
-
+
def setup_about_tab(self):
about_tab = QWidget()
about_layout = QVBoxLayout()
@@ -997,7 +754,7 @@ class SpotiFLACGUI(QWidget):
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
about_layout.addItem(spacer)
- footer_label = QLabel("v2.9 | May 2025")
+ footer_label = QLabel("v2.6 | May 2025")
footer_label.setStyleSheet("font-size: 12px; margin-top: 10px;")
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
@@ -1050,18 +807,6 @@ class SpotiFLACGUI(QWidget):
self.settings.setValue('service', service)
self.settings.sync()
self.log_output.append(f"Service setting saved: {self.service_dropdown.currentText()}")
-
- if hasattr(self, 'qobuz_region_dropdown'):
- self.qobuz_region_dropdown.setVisible(service == "qobuz")
-
- self.update_service_ui_visibility()
-
- 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_settings(self):
self.settings.setValue('output_path', self.output_dir.text().strip())
@@ -1338,7 +1083,6 @@ class SpotiFLACGUI(QWidget):
def start_download_worker(self, tracks_to_download, outpath):
service = self.service_dropdown.currentData()
- qobuz_region_val = self.qobuz_region_dropdown.currentData() if service == "qobuz" else self.qobuz_region
self.worker = DownloadWorker(
tracks_to_download,
@@ -1352,8 +1096,7 @@ class SpotiFLACGUI(QWidget):
self.use_album_subfolders,
self.use_fallback,
service,
- self.timeout_value,
- qobuz_region=qobuz_region_val
+ self.timeout_value
)
self.worker.finished.connect(self.on_download_finished)
self.worker.progress.connect(self.update_progress)
@@ -1374,12 +1117,17 @@ class SpotiFLACGUI(QWidget):
def update_progress(self, message, percentage):
if "Download progress:" in message or "Processing metadata..." in message:
current_text = self.log_output.toPlainText()
+
if current_text:
lines = current_text.split('\n')
- if lines and ("Download progress:" in lines[-1] or "Processing metadata..." in lines[-1]):
+
+ if "Download progress:" in lines[-1] or "Processing metadata..." in lines[-1]:
lines[-1] = message
+
new_text = '\n'.join(lines)
+
self.log_output.setPlainText(new_text)
+
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
else:
self.log_output.append(message)
@@ -1388,7 +1136,8 @@ class SpotiFLACGUI(QWidget):
else:
self.log_output.append(message)
- self.progress_bar.setValue(percentage)
+ if percentage > 0:
+ self.progress_bar.setValue(percentage)
def stop_download(self):
if hasattr(self, 'worker'):
diff --git a/SquidWTF.py b/SquidWTF.py
deleted file mode 100644
index 2ac8a97..0000000
--- a/SquidWTF.py
+++ /dev/null
@@ -1,277 +0,0 @@
-import requests
-from mutagen.flac import FLAC, Picture
-from datetime import datetime
-import sys
-import os
-
-def _safe_print(*args, **kwargs):
- if sys.stdout:
- try:
- print(*args, **kwargs)
- except UnicodeEncodeError:
- encoding = getattr(sys.stdout, 'encoding', None) or 'ascii'
-
- processed_args = []
- for arg in args:
- processed_args.append(str(arg).encode(encoding, 'replace').decode(encoding))
-
- processed_kwargs = {}
- for k, v in kwargs.items():
- if isinstance(v, str):
- processed_kwargs[k] = v.encode(encoding, 'replace').decode(encoding)
- else:
- processed_kwargs[k] = v
- try:
- print(*processed_args, **processed_kwargs)
- except Exception:
- pass
- except Exception:
- pass
-
-def _safe_stdout_write(data_to_write):
- if sys.stdout:
- try:
- sys.stdout.write(data_to_write)
- except UnicodeEncodeError:
- encoding = getattr(sys.stdout, 'encoding', None) or 'ascii'
- safe_data = data_to_write.encode(encoding, 'replace').decode(encoding)
- try:
- sys.stdout.write(safe_data)
- except Exception:
- pass
- except Exception:
- pass
-
-def _safe_flush():
- if sys.stdout:
- try:
- sys.stdout.flush()
- except Exception:
- pass
-
-def get_track_info(isrc, region="us"):
- _safe_print(f"Search: {isrc}")
- base_url = f"https://{region}.qobuz.squid.wtf"
- url = f"{base_url}/api/get-music?q={isrc}&offset=0"
- response = requests.get(url)
- data = response.json()
-
- if not data.get("success"):
- raise Exception("Failed to get track info")
-
- tracks = data["data"]["tracks"]["items"]
- if not tracks:
- _safe_print(f"Not Found: {isrc}")
- raise Exception(f"No tracks found for ISRC: {isrc}")
-
- track = None
- for item in tracks:
- if item["isrc"] == isrc:
- track = item
- break
-
- if not track:
- _safe_print(f"Not Found: {isrc}")
- raise Exception(f"No track with matching ISRC: {isrc}")
-
- _safe_print(f"Found: {track['title']} - {track['performer']['name']}")
- return track
-
-def search_track(title, artist, strict_match=False, region="us"):
- _safe_print(f"Search by title/artist: {title} - {artist}")
-
- search_query = f"{title} {artist}".replace("feat.", "").replace("ft.", "")
-
- base_url = f"https://{region}.qobuz.squid.wtf"
- url = f"{base_url}/api/get-music?q={search_query}&offset=0"
- response = requests.get(url)
- data = response.json()
-
- if not data.get("success"):
- raise Exception("Failed to search for track")
-
- tracks = data["data"]["tracks"]["items"]
- if not tracks:
- _safe_print(f"Not Found: {title} - {artist}")
- raise Exception(f"No tracks found for: {title} - {artist}")
-
- best_match = None
- title_lower = title.lower()
- artist_lower = artist.lower()
-
- for item in tracks:
- item_title = item["title"].lower()
- item_artist = item["performer"]["name"].lower()
-
- if title_lower == item_title and (artist_lower in item_artist or item_artist in artist_lower):
- best_match = item
- _safe_print(f"Found exact title match with artist: {item['title']} - {item['performer']['name']}")
- break
-
- if not best_match and not strict_match:
- for item in tracks:
- item_title = item["title"].lower()
- item_artist = item["performer"]["name"].lower()
-
- if title_lower in item_title and (artist_lower in item_artist or item_artist in artist_lower):
- best_match = item
- _safe_print(f"Found partial match: {item['title']} - {item['performer']['name']}")
- break
-
- if strict_match and best_match:
- item_artist = best_match["performer"]["name"].lower()
- if artist_lower not in item_artist and item_artist not in artist_lower:
- _safe_print(f"Artist mismatch in strict mode: Expected '{artist}', found '{best_match['performer']['name']}'")
- best_match = None
-
- if not best_match and not strict_match and tracks:
- best_match = tracks[0]
- _safe_print(f"No good match, using first result: {best_match['title']} - {best_match['performer']['name']}")
-
- if not best_match:
- _safe_print(f"Not Found: {title} - {artist}")
- raise Exception(f"No suitable track found for: {title} - {artist}")
-
- _safe_print(f"Found by title search: {best_match['title']} - {best_match['performer']['name']}")
- return best_match
-
-def get_download_url(track_id, region="us"):
- base_url = f"https://{region}.qobuz.squid.wtf"
- url = f"{base_url}/api/download-music?track_id={track_id}&quality=27"
- response = requests.get(url)
- data = response.json()
-
- if not data.get("success"):
- raise Exception("Failed to get download URL")
-
- return data["data"]["url"]
-
-def download_file(url, filename, progress_callback=None):
- directory = os.path.dirname(filename)
- if directory and not os.path.exists(directory):
- try:
- os.makedirs(directory, exist_ok=True)
- _safe_print(f"Created directory: {directory}")
- except Exception as e:
- raise Exception(f"Failed to create directory {directory}: {str(e)}")
-
- try:
- with open(filename, 'wb') as test_file:
- pass
- except Exception as e:
- raise Exception(f"Cannot write to file {filename}: {str(e)}")
-
- try:
- response = requests.get(url, stream=True)
-
- if response.status_code != 200:
- raise Exception(f"Failed to download file: {response.status_code}")
-
- total_size = int(response.headers.get('content-length', 0))
- downloaded = 0
-
- with open(filename, 'wb') as f:
- for chunk in response.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
- downloaded += len(chunk)
-
- if total_size > 0 and progress_callback:
- progress_callback(downloaded, total_size)
- elif total_size > 0:
- progress = (downloaded / total_size) * 100
- _safe_stdout_write(f"\rProgress Download: {progress:.1f}%")
- _safe_flush()
-
- if total_size > 0 and not progress_callback:
- _safe_stdout_write("\n")
-
- if not os.path.exists(filename) or os.path.getsize(filename) == 0:
- raise Exception(f"Download failed: File {filename} is empty or does not exist")
-
- return filename
- except Exception as e:
- if os.path.exists(filename):
- try:
- os.remove(filename)
- _safe_print(f"Removed incomplete file: {filename}")
- except:
- pass
- raise Exception(f"Download failed: {str(e)}")
-
-def embed_metadata(filename, track_info):
- if not os.path.exists(filename):
- raise Exception(f"Cannot embed metadata: File {filename} does not exist")
-
- try:
- _safe_print("Embedding Tags...")
- audio = FLAC(filename)
- audio.clear()
-
- audio["TITLE"] = track_info["title"]
- audio["ARTIST"] = track_info["performer"]["name"]
- audio["ALBUM"] = track_info["album"]["title"]
- audio["ALBUMARTIST"] = track_info["album"]["artist"]["name"]
- audio["TRACKNUMBER"] = str(track_info["track_number"])
- audio["LABEL"] = track_info["album"]["label"]["name"]
- audio["GENRE"] = track_info["album"]["genre"]["name"]
-
- release_date = datetime.fromtimestamp(track_info["album"]["released_at"]).strftime("%Y-%m-%d")
- release_year = release_date.split("-")[0]
-
- audio["DATE"] = release_date
- audio["YEAR"] = release_year
- audio["ISRC"] = track_info["isrc"]
- audio["COPYRIGHT"] = track_info["copyright"]
-
- if track_info["album"]["image"]["large"]:
- try:
- cover_data = download_cover_image(track_info["album"]["image"]["large"])
- picture = Picture()
- picture.type = 3
- picture.mime = "image/jpeg"
- picture.desc = ""
- picture.data = cover_data
-
- audio.add_picture(picture)
- except Exception as e:
- _safe_print(f"Warning: Could not add cover image: {str(e)}")
-
- audio.save()
- except Exception as e:
- raise Exception(f"Failed to embed metadata: {str(e)}")
-
-def download_cover_image(url):
- response = requests.get(url)
-
- if response.status_code != 200:
- raise Exception(f"Failed to download cover image: {response.status_code}")
-
- return response.content
-
-def main():
- try:
- isrc = "USQX92500261"
- region = "us"
-
- track_info = get_track_info(isrc, region)
- track_id = track_info["id"]
-
- if track_info["isrc"] != isrc:
- raise Exception(f"ISRC mismatch: {track_info['isrc']} != {isrc}")
-
- download_url = get_download_url(track_id, region)
-
- filename = f"{track_info['title']} - {track_info['performer']['name']}.flac"
- filename = filename.replace('/', '_').replace('\\', '_')
-
- download_file(download_url, filename)
- embed_metadata(filename, track_info)
-
- print("Downloaded Successfully!")
-
- except Exception as e:
- print(f"Error: {e}")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/getMetadata.py b/getMetadata.py
index a40469c..c4ab2c8 100644
--- a/getMetadata.py
+++ b/getMetadata.py
@@ -319,8 +319,6 @@ def format_track_data(track_data):
image_url = track_data.get('album', {}).get('images', [{}])[0].get('url', '') if track_data.get('album', {}).get('images') else ''
- isrc = track_data.get('external_ids', {}).get('isrc', '')
-
return {
"track": {
"artists": ", ".join(artists),
@@ -330,8 +328,7 @@ def format_track_data(track_data):
"images": image_url,
"release_date": track_data.get('album', {}).get('release_date', ''),
"track_number": track_data.get('track_number', 0),
- "external_urls": track_data.get('external_urls', {}).get('spotify', ''),
- "isrc": isrc
+ "external_urls": track_data.get('external_urls', {}).get('spotify', '')
}
}
@@ -347,20 +344,6 @@ def format_album_data(album_data):
track_artists = []
for artist in track.get('artists', []):
track_artists.append(artist['name'])
-
- track_id = track.get('id', '')
- track_isrc = ''
-
- if track_id and album_data.get('_token'):
- try:
- full_track_data = get_json_from_api(
- track_base_url.format(track_id),
- album_data.get('_token')
- )
- if full_track_data:
- track_isrc = full_track_data.get('external_ids', {}).get('isrc', '')
- except:
- pass
track_list.append({
"artists": ", ".join(track_artists),
@@ -370,8 +353,7 @@ def format_album_data(album_data):
"images": image_url,
"release_date": album_data.get('release_date', ''),
"track_number": track.get('track_number', 0),
- "external_urls": track.get('external_urls', {}).get('spotify', ''),
- "isrc": track_isrc
+ "external_urls": track.get('external_urls', {}).get('spotify', '')
})
album_info = {
@@ -407,8 +389,6 @@ def format_playlist_data(playlist_data):
if track.get('album', {}).get('images'):
track_image = track.get('album', {}).get('images', [{}])[0].get('url', '')
- track_isrc = track.get('external_ids', {}).get('isrc', '')
-
track_list.append({
"artists": ", ".join(artists),
"name": track.get('name', ''),
@@ -417,8 +397,7 @@ def format_playlist_data(playlist_data):
"images": track_image,
"release_date": track.get('album', {}).get('release_date', ''),
"track_number": track.get('track_number', 0),
- "external_urls": track.get('external_urls', {}).get('spotify', ''),
- "isrc": track_isrc
+ "external_urls": track.get('external_urls', {}).get('spotify', '')
})
playlist_info = {
@@ -464,9 +443,9 @@ def get_filtered_data(spotify_url, batch=False, delay=1.0):
return {"error": "Failed to get raw data"}
if __name__ == '__main__':
- playlist = "https://open.spotify.com/playlist/37i9dQZEVXbNG2KDcFcKOF"
- album = "https://open.spotify.com/album/6J84szYCnMfzEcvIcfWMFL"
- song = "https://open.spotify.com/track/7so0lgd0zP2Sbgs2d7a1SZ"
+ playlist = "https://open.spotify.com/playlist/5Qvz8wZIRYbEUUFoPueKI5"
+ album = "https://open.spotify.com/album/7kFyd5oyJdVX2pIi6P4iHE"
+ song = "https://open.spotify.com/track/4wJ5Qq0jBN4ajy7ouZIV1c"
filtered_playlist = get_filtered_data(playlist, batch=True, delay=0.1)
print(json.dumps(filtered_playlist, indent=2))
diff --git a/version.json b/version.json
index 904ad8b..acb409b 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "version": "2.9"
+ "version": "2.6"
}