diff --git a/SpotiFLAC.py b/SpotiFLAC.py
index d7e0fe6..5c4bbb2 100644
--- a/SpotiFLAC.py
+++ b/SpotiFLAC.py
@@ -20,7 +20,8 @@ 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 qobuzDL import QobuzDownloader
+from qobuzAutoDL import QobuzDownloader as QobuzAutoDownloader
+from qobuzRegionDL import QobuzDownloader as QobuzRegionDownloader
from tidalDL import TidalDownloader
from deezerDL import DeezerDownloader
from amazonDL import LucidaDownloader
@@ -35,6 +36,7 @@ class Track:
duration_ms: int
id: str
isrc: str = ""
+ release_date: str = ""
class MetadataFetchWorker(QThread):
finished = pyqtSignal(dict)
@@ -62,7 +64,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_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us"):
+ use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us", qobuz_mode="auto"):
super().__init__()
self.tracks = tracks
self.outpath = outpath
@@ -76,6 +78,7 @@ class DownloadWorker(QThread):
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 = []
@@ -92,7 +95,10 @@ class DownloadWorker(QThread):
def run(self):
try:
if self.service == "qobuz":
- downloader = QobuzDownloader(self.qobuz_region)
+ if self.qobuz_mode == "auto":
+ downloader = QobuzAutoDownloader()
+ else:
+ downloader = QobuzRegionDownloader(self.qobuz_region)
elif self.service == "tidal":
downloader = TidalDownloader()
elif self.service == "deezer":
@@ -361,13 +367,17 @@ class QobuzStatusChecker(QThread):
status_updated = pyqtSignal(bool)
error = pyqtSignal(str)
- def __init__(self, region="us"):
+ def __init__(self, region="us", mode="auto"):
super().__init__()
self.region = region
+ self.mode = mode
def run(self):
try:
- response = requests.get(f"https://{self.region}.qobuz.squid.wtf", timeout=5)
+ 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)}")
@@ -569,8 +579,11 @@ class QobuzRegionComboBox(QComboBox):
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': 'us', 'name': 'North America', 'icon': 'us.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:
@@ -612,7 +625,7 @@ class QobuzRegionComboBox(QComboBox):
for region in self.regions:
region_id = region['id']
- checker = QobuzStatusChecker(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
@@ -627,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget):
def __init__(self):
super().__init__()
- self.current_version = "4.4"
+ self.current_version = "4.5"
self.tracks = []
self.all_tracks = []
self.reset_state()
@@ -642,8 +655,11 @@ class SpotiFLACGUI(QWidget):
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)
@@ -662,6 +678,9 @@ class SpotiFLACGUI(QWidget):
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):
@@ -762,11 +781,74 @@ class SpotiFLACGUI(QWidget):
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)
- self.track_list.addItem(f"{i}. {track.title} - {track.artists} • {duration}")
+ 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")
@@ -963,15 +1045,16 @@ class SpotiFLACGUI(QWidget):
def setup_settings_tab(self):
settings_tab = QWidget()
settings_layout = QVBoxLayout()
- settings_layout.setSpacing(10)
- settings_layout.setContentsMargins(9, 9, 9, 9)
+ settings_layout.setSpacing(4)
+ settings_layout.setContentsMargins(10, 10, 10, 10)
output_group = QWidget()
output_layout = QVBoxLayout(output_group)
- output_layout.setSpacing(5)
+ output_layout.setSpacing(2)
+ output_layout.setContentsMargins(0, 0, 0, 0)
output_label = QLabel('Output Directory')
- output_label.setStyleSheet("font-weight: bold;")
+ output_label.setStyleSheet("font-weight: bold; margin-top: 0px; margin-bottom: 5px;")
output_layout.addWidget(output_label)
output_dir_layout = QHBoxLayout()
@@ -985,18 +1068,67 @@ class SpotiFLACGUI(QWidget):
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(5)
+ file_layout.setSpacing(2)
+ file_layout.setContentsMargins(0, 0, 0, 0)
file_label = QLabel('File Settings')
- file_label.setStyleSheet("font-weight: bold;")
+ file_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
file_layout.addWidget(file_label)
format_layout = QHBoxLayout()
@@ -1027,7 +1159,9 @@ class SpotiFLACGUI(QWidget):
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)
@@ -1039,14 +1173,16 @@ class SpotiFLACGUI(QWidget):
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 for Album')
+ 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)
@@ -1059,10 +1195,11 @@ class SpotiFLACGUI(QWidget):
auth_group = QWidget()
auth_layout = QVBoxLayout(auth_group)
- auth_layout.setSpacing(5)
+ auth_layout.setSpacing(2)
+ auth_layout.setContentsMargins(0, 0, 0, 0)
auth_label = QLabel('Service Settings')
- auth_label.setStyleSheet("font-weight: bold;")
+ auth_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
auth_layout.addWidget(auth_label)
service_fallback_layout = QHBoxLayout()
@@ -1076,13 +1213,25 @@ class SpotiFLACGUI(QWidget):
service_fallback_layout.addSpacing(10)
- region_label = QLabel('Region:')
+ 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(region_label)
+ service_fallback_layout.addWidget(self.region_label)
service_fallback_layout.addWidget(self.qobuz_region_dropdown)
- region_label.hide()
+ self.qobuz_mode_label.hide()
+ self.qobuz_mode_dropdown.hide()
+ self.region_label.hide()
self.qobuz_region_dropdown.hide()
service_fallback_layout.addStretch()
@@ -1093,7 +1242,10 @@ class SpotiFLACGUI(QWidget):
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_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()
@@ -1105,7 +1257,7 @@ class SpotiFLACGUI(QWidget):
theme_tab = QWidget()
theme_layout = QVBoxLayout()
theme_layout.setSpacing(8)
- theme_layout.setContentsMargins(15, 15, 15, 15)
+ theme_layout.setContentsMargins(8, 15, 15, 15)
grid_layout = QVBoxLayout()
@@ -1302,8 +1454,7 @@ class SpotiFLACGUI(QWidget):
about_layout.addWidget(section_widget)
- footer_label = QLabel(f"v{self.current_version} | August 2025")
- footer_label.setStyleSheet("font-size: 12px; margin-top: 20px;")
+ footer_label = QLabel(f"v{self.current_version} | September 2025")
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
about_tab.setLayout(about_layout)
@@ -1318,26 +1469,38 @@ class SpotiFLACGUI(QWidget):
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
-
- region_label = None
- for widget in self.qobuz_region_dropdown.parentWidget().children():
- if isinstance(widget, QLabel) and widget.text() == "Region:":
- region_label = widget
- break
if service == "qobuz":
- if region_label:
- region_label.show()
- self.qobuz_region_dropdown.show()
- elif service == "deezer":
- if region_label:
- region_label.hide()
- self.qobuz_region_dropdown.hide()
+ self.qobuz_mode_label.show()
+ self.qobuz_mode_dropdown.show()
+ self.update_qobuz_mode_ui()
else:
- if region_label:
- region_label.hide()
+ 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):
@@ -1376,6 +1539,22 @@ class SpotiFLACGUI(QWidget):
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()
@@ -1437,7 +1616,8 @@ class SpotiFLACGUI(QWidget):
track_number=1,
duration_ms=track_data.get("duration_ms", 0),
id=track_id,
- isrc=track_data.get("isrc", "")
+ isrc=track_data.get("isrc", ""),
+ release_date=track_data.get("release_date", "")
)
self.tracks = [track]
@@ -1470,7 +1650,8 @@ class SpotiFLACGUI(QWidget):
track_number=track["track_number"],
duration_ms=track.get("duration_ms", 0),
id=track_id,
- isrc=track.get("isrc", "")
+ isrc=track.get("isrc", ""),
+ release_date=track.get("release_date", "")
))
self.all_tracks = self.tracks.copy()
@@ -1501,7 +1682,8 @@ class SpotiFLACGUI(QWidget):
track_number=track.get("track_number", len(self.tracks) + 1),
duration_ms=track.get("duration_ms", 0),
id=track_id,
- isrc=track.get("isrc", "")
+ isrc=track.get("isrc", ""),
+ release_date=track.get("release_date", "")
))
self.all_tracks = self.tracks.copy()
@@ -1674,6 +1856,7 @@ class SpotiFLACGUI(QWidget):
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,
@@ -1687,7 +1870,8 @@ class SpotiFLACGUI(QWidget):
self.use_artist_subfolders,
self.use_album_subfolders,
service,
- qobuz_region
+ qobuz_region,
+ qobuz_mode
)
self.worker.finished.connect(self.on_download_finished)
self.worker.progress.connect(self.update_progress)
@@ -1773,9 +1957,7 @@ class SpotiFLACGUI(QWidget):
if track in self.all_tracks:
self.all_tracks.remove(track)
- if self.is_playlist:
- for i, track in enumerate(self.all_tracks, 1):
- track.track_number = i
+
self.update_track_list_display()
diff --git a/amazonDL.py b/amazonDL.py
index 671ad51..4ac7e4f 100644
--- a/amazonDL.py
+++ b/amazonDL.py
@@ -5,9 +5,13 @@ import re
import base64
import urllib3
from urllib.parse import unquote
+from random import randrange
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+def get_random_user_agent():
+ return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
+
def extract_data(html, patterns):
for pattern in patterns:
if match := re.search(pattern, html):
@@ -17,7 +21,7 @@ def extract_data(html, patterns):
def download_track(track_id, service="amazon", output_dir="."):
client = requests.Session()
client.verify = False
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
+ headers = {'User-Agent': get_random_user_agent()}
try:
spotify_url = f"https://open.spotify.com/track/{track_id}"
@@ -39,7 +43,7 @@ def download_track(track_id, service="amazon", output_dir="."):
decoded_token = token
clean_url = url.replace('\\/', '/')
- print(f"Starting download for: {clean_url}")
+ print(f"Fetching: {clean_url}")
request_data = {
"account": {"id": "auto", "type": "country"},
@@ -61,18 +65,18 @@ def download_track(track_id, service="amazon", output_dir="."):
raise Exception(f"Request failed: {data.get('error', 'Unknown error')}")
completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}"
- print("Processing track...")
+ print("Fetching URL...")
while True:
resp = client.get(completion_url, headers=headers).json()
if resp["status"] == "completed":
- print("Processing completed!")
+ print("URL found")
break
elif resp["status"] == "error":
raise Exception(f"Processing failed: {resp.get('message', 'Unknown error')}")
elif progress := resp.get("progress"):
percent = int((progress.get("current", 0) / progress.get("total", 100)) * 100)
- print(f"Progress: {percent}%")
+ print(f"\r{percent}%", end="")
time.sleep(1)
download_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}/download"
@@ -88,14 +92,15 @@ def download_track(track_id, service="amazon", output_dir="."):
file_name = file_name.strip()
file_path = os.path.join(output_dir, file_name)
- print(f"Downloading: {file_name}")
+ print(f"Downloading...")
with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
- print(f"Download completed: {file_path}")
+ print("Download complete")
+ print("Done")
return file_path
except Exception as e:
@@ -116,7 +121,8 @@ class LucidaDownloader:
raise Exception(f"Amazon Music download failed: {str(e)}")
if __name__ == "__main__":
+ print("=== AmazonDL - Amazon Music Downloader ===")
track_id = "2plbrEY59IikOBgBGLjaoe"
service = "amazon"
- download_track(track_id, service)
+ download_track(track_id, service)
\ No newline at end of file
diff --git a/au.svg b/au.svg
new file mode 100644
index 0000000..96e8076
--- /dev/null
+++ b/au.svg
@@ -0,0 +1,8 @@
+
diff --git a/br.svg b/br.svg
new file mode 100644
index 0000000..719a763
--- /dev/null
+++ b/br.svg
@@ -0,0 +1,45 @@
+
diff --git a/deezerDL.py b/deezerDL.py
index 4d0d22b..6c4e422 100644
--- a/deezerDL.py
+++ b/deezerDL.py
@@ -3,12 +3,16 @@ import asyncio
import os
import sys
from mutagen.flac import FLAC
+from random import randrange
+
+def get_random_user_agent():
+ return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
class DeezerDownloader:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ 'User-Agent': get_random_user_agent()
})
self.progress_callback = None
diff --git a/getMetadata.py b/getMetadata.py
index 26247c8..fd69339 100644
--- a/getMetadata.py
+++ b/getMetadata.py
@@ -58,7 +58,7 @@ playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
album_base_url = 'https://api.spotify.com/v1/albums/{}'
track_base_url = 'https://api.spotify.com/v1/tracks/{}'
headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ 'User-Agent': get_random_user_agent(),
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
diff --git a/jp.svg b/jp.svg
new file mode 100644
index 0000000..cc1c181
--- /dev/null
+++ b/jp.svg
@@ -0,0 +1,11 @@
+
diff --git a/qobuzAutoDL.py b/qobuzAutoDL.py
new file mode 100644
index 0000000..315c184
--- /dev/null
+++ b/qobuzAutoDL.py
@@ -0,0 +1,251 @@
+import requests
+import time
+import os
+import re
+from datetime import datetime
+from mutagen.flac import FLAC, Picture
+from mutagen.id3 import PictureType
+from random import randrange
+
+def get_random_user_agent():
+ return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
+
+class ProgressCallback:
+ def __call__(self, current, total):
+ if total > 0:
+ percent = (current / total) * 100
+ print(f"\r{percent:.2f}% ({current}/{total})", end="")
+ else:
+ print(f"\r{current / (1024 * 1024):.2f} MB", end="")
+
+class QobuzDownloader:
+ def __init__(self, timeout=30):
+ self.timeout = timeout
+ self.session = requests.Session()
+ self.headers = {
+ 'User-Agent': get_random_user_agent()
+ }
+ self.base_api_url = "https://qobuz.squid.wtf/api"
+ self.download_chunk_size = 256 * 1024
+ self.progress_callback = ProgressCallback()
+
+ def set_progress_callback(self, callback):
+ self.progress_callback = callback
+
+ def sanitize_filename(self, filename):
+ if not filename:
+ return "Unknown Track"
+ sanitized = re.sub(r'[\\/*?:"<>|]', "", str(filename))
+ return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track"
+
+ def get_track_info(self, isrc):
+ print(f"Fetching: {isrc}")
+ search_url = f"{self.base_api_url}/get-music"
+ params = {'q': isrc, 'offset': 0, 'limit': 10, 'region': 'auto'}
+
+ try:
+ response = self.session.get(search_url, params=params, timeout=self.timeout)
+ response.raise_for_status()
+ data = response.json()
+
+ selected_track = None
+ if data and data.get("success"):
+ items = data.get("data", {}).get("tracks", {}).get("items", [])
+ priority = {24: 1, 16: 2}
+ for track in items:
+ if track.get("isrc") == isrc:
+ current_prio = priority.get(track.get("maximum_bit_depth"), 3)
+ if selected_track is None or current_prio < priority.get(selected_track.get("maximum_bit_depth"), 3):
+ selected_track = track
+ if current_prio == 1:
+ break
+
+ if not selected_track:
+ raise Exception(f"Track not found: {isrc}")
+
+ title = selected_track.get('title', 'Unknown')
+ bit_depth = selected_track.get('maximum_bit_depth', 'Unknown')
+ print(f"Found: {title} ({bit_depth}b)")
+ return selected_track
+
+ except requests.exceptions.RequestException as e:
+ raise Exception(f"Request error: {e}")
+ except Exception as e:
+ raise Exception(f"Error: {e}")
+
+ def get_download_url(self, track_id):
+ print("Fetching URL...")
+ download_api_url = f"{self.base_api_url}/download-music"
+ params = {'track_id': track_id, 'quality': 27, 'region': 'auto'}
+
+ try:
+ response = self.session.get(download_api_url, params=params, timeout=self.timeout)
+ response.raise_for_status()
+ data = response.json()
+
+ if data and data.get("success") and data.get("data", {}).get("url"):
+ download_url = data["data"]["url"]
+ print("URL found")
+ return download_url
+ else:
+ error_msg = data.get('error', {}).get('message', 'Unknown API error')
+ raise Exception(f"API error: {error_msg}")
+
+ except requests.exceptions.RequestException as e:
+ raise Exception(f"Request error: {e}")
+ except Exception as e:
+ raise Exception(f"Error: {e}")
+
+ def download(self, isrc, output_dir=".", is_paused_callback=None, is_stopped_callback=None):
+ if output_dir != ".":
+ try:
+ os.makedirs(output_dir, exist_ok=True)
+ except OSError as e:
+ raise Exception(f"Directory error: {e}")
+
+ track_info = self.get_track_info(isrc)
+ track_id = track_info.get("id")
+
+ if not track_id:
+ raise Exception("No track ID found")
+
+ artist_name = self.sanitize_filename(track_info.get('performer', {}).get('name'))
+ track_title = self.sanitize_filename(track_info.get('title'))
+ output_filename = os.path.join(output_dir, f"{artist_name} - {track_title}.flac")
+
+ if os.path.exists(output_filename):
+ file_size = os.path.getsize(output_filename)
+ if file_size > 0:
+ print(f"File already exists: {output_filename} ({file_size / (1024 * 1024):.2f} MB)")
+ return output_filename
+
+ download_url = self.get_download_url(track_id)
+ temp_filename = output_filename + ".part"
+
+ print(f"Downloading...")
+ try:
+ response = self.session.get(download_url, timeout=900)
+ response.raise_for_status()
+
+ if is_stopped_callback and is_stopped_callback():
+ raise Exception("Download stopped")
+
+ while is_paused_callback and is_paused_callback():
+ time.sleep(0.1)
+ if is_stopped_callback and is_stopped_callback():
+ raise Exception("Download stopped")
+
+ with open(temp_filename, 'wb') as f:
+ f.write(response.content)
+
+ downloaded_size = len(response.content)
+ total_size = downloaded_size
+
+ if self.progress_callback:
+ self.progress_callback(downloaded_size, total_size)
+
+ os.rename(temp_filename, output_filename)
+ print("Download complete")
+
+ except requests.exceptions.RequestException as e:
+ if os.path.exists(temp_filename):
+ os.remove(temp_filename)
+ raise Exception(f"Download failed: {e}")
+ except Exception as e:
+ if os.path.exists(temp_filename):
+ os.remove(temp_filename)
+ raise Exception(f"File error: {e}")
+
+ print("Adding metadata...")
+ try:
+ self._embed_metadata(output_filename, track_info)
+ print("Metadata saved")
+ except Exception as e:
+ print(f"Tagging failed: {e}")
+
+ print(f"Done")
+ return output_filename
+
+ def _embed_metadata(self, filename, track_info):
+ try:
+ audio = FLAC(filename)
+ audio.delete()
+ audio.clear_pictures()
+
+ album_info = track_info.get('album', {})
+ artist = track_info.get('performer', {}).get('name')
+
+ if track_info.get('title'):
+ audio['TITLE'] = track_info['title']
+ if artist:
+ audio['ARTIST'] = artist
+ if album_info.get('title'):
+ audio['ALBUM'] = album_info['title']
+ if album_info.get('artist', {}).get('name', artist):
+ audio['ALBUMARTIST'] = album_info.get('artist', {}).get('name', artist)
+ if track_info.get('track_number'):
+ audio['TRACKNUMBER'] = str(track_info['track_number'])
+ if track_info.get('release_date_original'):
+ audio['DATE'] = track_info['release_date_original']
+ try:
+ audio['YEAR'] = str(datetime.strptime(track_info['release_date_original'], '%Y-%m-%d').year)
+ except ValueError:
+ pass
+ if album_info.get('genre', {}).get('name'):
+ audio['GENRE'] = album_info['genre']['name']
+ if track_info.get('copyright'):
+ audio['COPYRIGHT'] = track_info['copyright']
+ if track_info.get('isrc'):
+ audio['ISRC'] = track_info['isrc']
+ if album_info.get('label', {}).get('name'):
+ audio['ORGANIZATION'] = album_info['label']['name']
+
+ img_info = album_info.get('image', {})
+ cover_url = img_info.get('large') or img_info.get('small') or img_info.get('thumbnail')
+ if cover_url:
+ try:
+ img_response = self.session.get(cover_url, timeout=30)
+ img_response.raise_for_status()
+ mime_type = img_response.headers.get('Content-Type', 'image/jpeg').lower()
+ if mime_type in ['image/jpeg', 'image/png']:
+ picture = Picture()
+ picture.data = img_response.content
+ picture.type = PictureType.COVER_FRONT
+ picture.mime = mime_type
+ audio.add_picture(picture)
+ print("Cover added")
+ except Exception as e:
+ print(f"Cover error: {str(e)}")
+
+ audio.save()
+
+ except Exception as e:
+ raise Exception(f"Metadata error: {e}")
+
+def main():
+ print("=== QobuzDL - Qobuz Downloader (Auto) ===")
+ downloader = QobuzDownloader()
+
+ isrc = "USAT22409172"
+ output_dir = "."
+
+ try:
+ downloaded_file = downloader.download(isrc, output_dir)
+ print(f"Success: File saved as {downloaded_file}")
+ except Exception as e:
+ print(f"Error: {str(e)}")
+
+if __name__ == "__main__":
+ try:
+ import sys
+ if sys.platform == "win32":
+ import os
+ os.system("chcp 65001 > nul")
+ try:
+ sys.stdout.reconfigure(encoding='utf-8')
+ except:
+ pass
+ except:
+ pass
+
+ main()
\ No newline at end of file
diff --git a/qobuzDL.py b/qobuzRegionDL.py
similarity index 93%
rename from qobuzDL.py
rename to qobuzRegionDL.py
index ada4e74..bafc02d 100644
--- a/qobuzDL.py
+++ b/qobuzRegionDL.py
@@ -5,6 +5,10 @@ import re
from datetime import datetime
from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType
+from random import randrange
+
+def get_random_user_agent():
+ return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
class ProgressCallback:
def __call__(self, current, total):
@@ -16,16 +20,16 @@ class ProgressCallback:
class QobuzDownloader:
def __init__(self, region="us", timeout=30):
- if region not in ["eu", "us"]:
- raise ValueError("Region must be either 'us' or 'eu'")
+ if region not in ["us", "eu", "br", "jp", "au"]:
+ raise ValueError("Region must be one of: 'us', 'eu', 'br', 'jp', 'au'")
self.region = region
self.timeout = timeout
self.session = requests.Session()
self.headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ 'User-Agent': get_random_user_agent()
}
- self.base_api_url = f"https://{region}.qobuz.squid.wtf/api"
+ self.base_api_url = f"https://{region}.qqdl.site/api"
self.download_chunk_size = 256 * 1024
self.progress_callback = ProgressCallback()
@@ -223,7 +227,7 @@ class QobuzDownloader:
raise Exception(f"Metadata error: {e}")
def main():
- print("=== QobuzDL - Qobuz Downloader ===")
+ print("=== QobuzDL - Qobuz Downloader (Region) ===")
downloader = QobuzDownloader(region="us")
isrc = "USAT22409172"