This commit is contained in:
afkarxyz
2025-09-11 13:26:52 +07:00
parent cd2500d1df
commit 49a8de1b35
9 changed files with 573 additions and 62 deletions
+229 -47
View File
@@ -20,7 +20,8 @@ from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException 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 tidalDL import TidalDownloader
from deezerDL import DeezerDownloader from deezerDL import DeezerDownloader
from amazonDL import LucidaDownloader from amazonDL import LucidaDownloader
@@ -35,6 +36,7 @@ class Track:
duration_ms: int duration_ms: int
id: str id: str
isrc: str = "" isrc: str = ""
release_date: str = ""
class MetadataFetchWorker(QThread): class MetadataFetchWorker(QThread):
finished = pyqtSignal(dict) 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, 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, 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__() super().__init__()
self.tracks = tracks self.tracks = tracks
self.outpath = outpath self.outpath = outpath
@@ -76,6 +78,7 @@ class DownloadWorker(QThread):
self.use_album_subfolders = use_album_subfolders self.use_album_subfolders = use_album_subfolders
self.service = service self.service = service
self.qobuz_region = qobuz_region self.qobuz_region = qobuz_region
self.qobuz_mode = qobuz_mode
self.is_paused = False self.is_paused = False
self.is_stopped = False self.is_stopped = False
self.failed_tracks = [] self.failed_tracks = []
@@ -92,7 +95,10 @@ class DownloadWorker(QThread):
def run(self): def run(self):
try: try:
if self.service == "qobuz": 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": elif self.service == "tidal":
downloader = TidalDownloader() downloader = TidalDownloader()
elif self.service == "deezer": elif self.service == "deezer":
@@ -361,13 +367,17 @@ class QobuzStatusChecker(QThread):
status_updated = pyqtSignal(bool) status_updated = pyqtSignal(bool)
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__(self, region="us"): def __init__(self, region="us", mode="auto"):
super().__init__() super().__init__()
self.region = region self.region = region
self.mode = mode
def run(self): def run(self):
try: 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) self.status_updated.emit(response.status_code == 200)
except Exception as e: except Exception as e:
self.error.emit(f"Error checking Qobuz status: {str(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__)) current_dir = os.path.dirname(os.path.abspath(__file__))
self.regions = [ self.regions = [
{'id': 'us', 'name': 'USA', 'icon': 'us.svg', 'online': False},
{'id': 'eu', 'name': 'Europe', 'icon': 'eu.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: for region in self.regions:
@@ -612,7 +625,7 @@ class QobuzRegionComboBox(QComboBox):
for region in self.regions: for region in self.regions:
region_id = region['id'] 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.status_updated.connect(lambda status, rid=region_id: self.handle_status_update(rid, status))
checker.start() checker.start()
self.status_checkers[region_id] = checker self.status_checkers[region_id] = checker
@@ -627,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget): class SpotiFLACGUI(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "4.4" self.current_version = "4.5"
self.tracks = [] self.tracks = []
self.all_tracks = [] self.all_tracks = []
self.reset_state() self.reset_state()
@@ -642,8 +655,11 @@ class SpotiFLACGUI(QWidget):
self.use_album_subfolders = self.settings.value('use_album_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.service = self.settings.value('service', 'tidal')
self.qobuz_region = self.settings.value('qobuz_region', 'us') 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.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
self.current_theme_color = self.settings.value('theme_color', '#2196F3') 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.elapsed_time = QTime(0, 0, 0)
self.timer = QTimer(self) self.timer = QTimer(self)
@@ -662,6 +678,9 @@ class SpotiFLACGUI(QWidget):
if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value: if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value:
combobox.setCurrentIndex(i) combobox.setCurrentIndex(i)
return True return True
if combobox.itemData(i, Qt.ItemDataRole.UserRole) == target_value:
combobox.setCurrentIndex(i)
return True
return False return False
def check_updates(self): def check_updates(self):
@@ -762,11 +781,74 @@ class SpotiFLACGUI(QWidget):
self.update_track_list_display() 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): def update_track_list_display(self):
self.track_list.clear() self.track_list.clear()
for i, track in enumerate(self.tracks, 1): for i, track in enumerate(self.tracks, 1):
duration = self.format_duration(track.duration_ms) 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): def browse_output(self):
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory") directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
@@ -963,15 +1045,16 @@ class SpotiFLACGUI(QWidget):
def setup_settings_tab(self): def setup_settings_tab(self):
settings_tab = QWidget() settings_tab = QWidget()
settings_layout = QVBoxLayout() settings_layout = QVBoxLayout()
settings_layout.setSpacing(10) settings_layout.setSpacing(4)
settings_layout.setContentsMargins(9, 9, 9, 9) settings_layout.setContentsMargins(10, 10, 10, 10)
output_group = QWidget() output_group = QWidget()
output_layout = QVBoxLayout(output_group) 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 = 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_layout.addWidget(output_label)
output_dir_layout = QHBoxLayout() output_dir_layout = QHBoxLayout()
@@ -985,18 +1068,67 @@ class SpotiFLACGUI(QWidget):
self.output_browse.clicked.connect(self.browse_output) self.output_browse.clicked.connect(self.browse_output)
output_dir_layout.addWidget(self.output_dir) output_dir_layout.addWidget(self.output_dir)
output_dir_layout.addSpacing(5)
output_dir_layout.addWidget(self.output_browse) output_dir_layout.addWidget(self.output_browse)
output_layout.addLayout(output_dir_layout) output_layout.addLayout(output_dir_layout)
settings_layout.addWidget(output_group) 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_group = QWidget()
file_layout = QVBoxLayout(file_group) 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 = 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) file_layout.addWidget(file_label)
format_layout = QHBoxLayout() format_layout = QHBoxLayout()
@@ -1027,7 +1159,9 @@ class SpotiFLACGUI(QWidget):
format_layout.addWidget(format_label) format_layout.addWidget(format_label)
format_layout.addWidget(self.title_artist_radio) format_layout.addWidget(self.title_artist_radio)
format_layout.addSpacing(10)
format_layout.addWidget(self.artist_title_radio) format_layout.addWidget(self.artist_title_radio)
format_layout.addSpacing(10)
format_layout.addWidget(self.title_only_radio) format_layout.addWidget(self.title_only_radio)
format_layout.addStretch() format_layout.addStretch()
file_layout.addLayout(format_layout) 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.setChecked(self.use_artist_subfolders)
self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting) self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting)
checkbox_layout.addWidget(self.artist_subfolder_checkbox) checkbox_layout.addWidget(self.artist_subfolder_checkbox)
checkbox_layout.addSpacing(10)
self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)') self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)')
self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.album_subfolder_checkbox.setChecked(self.use_album_subfolders) self.album_subfolder_checkbox.setChecked(self.use_album_subfolders)
self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting) self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting)
checkbox_layout.addWidget(self.album_subfolder_checkbox) 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.setCursor(Qt.CursorShape.PointingHandCursor)
self.track_number_checkbox.setChecked(self.use_track_numbers) self.track_number_checkbox.setChecked(self.use_track_numbers)
self.track_number_checkbox.toggled.connect(self.save_track_numbering) self.track_number_checkbox.toggled.connect(self.save_track_numbering)
@@ -1059,10 +1195,11 @@ class SpotiFLACGUI(QWidget):
auth_group = QWidget() auth_group = QWidget()
auth_layout = QVBoxLayout(auth_group) 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 = 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) auth_layout.addWidget(auth_label)
service_fallback_layout = QHBoxLayout() service_fallback_layout = QHBoxLayout()
@@ -1076,13 +1213,25 @@ class SpotiFLACGUI(QWidget):
service_fallback_layout.addSpacing(10) 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 = QobuzRegionComboBox()
self.qobuz_region_dropdown.currentIndexChanged.connect(self.save_qobuz_region_setting) 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) 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() self.qobuz_region_dropdown.hide()
service_fallback_layout.addStretch() service_fallback_layout.addStretch()
@@ -1093,7 +1242,10 @@ class SpotiFLACGUI(QWidget):
settings_tab.setLayout(settings_layout) settings_tab.setLayout(settings_layout)
self.tab_widget.addTab(settings_tab, "Settings") self.tab_widget.addTab(settings_tab, "Settings")
self.set_combobox_value(self.service_dropdown, self.service) 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() self.update_service_ui()
@@ -1105,7 +1257,7 @@ class SpotiFLACGUI(QWidget):
theme_tab = QWidget() theme_tab = QWidget()
theme_layout = QVBoxLayout() theme_layout = QVBoxLayout()
theme_layout.setSpacing(8) theme_layout.setSpacing(8)
theme_layout.setContentsMargins(15, 15, 15, 15) theme_layout.setContentsMargins(8, 15, 15, 15)
grid_layout = QVBoxLayout() grid_layout = QVBoxLayout()
@@ -1302,8 +1454,7 @@ class SpotiFLACGUI(QWidget):
about_layout.addWidget(section_widget) about_layout.addWidget(section_widget)
footer_label = QLabel(f"v{self.current_version} | August 2025") footer_label = QLabel(f"v{self.current_version} | September 2025")
footer_label.setStyleSheet("font-size: 12px; margin-top: 20px;")
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter) about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
about_tab.setLayout(about_layout) about_tab.setLayout(about_layout)
@@ -1318,26 +1469,38 @@ class SpotiFLACGUI(QWidget):
self.update_service_ui() self.update_service_ui()
self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}") 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): def update_service_ui(self):
service = self.service 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 service == "qobuz":
if region_label: self.qobuz_mode_label.show()
region_label.show() self.qobuz_mode_dropdown.show()
self.qobuz_region_dropdown.show() self.update_qobuz_mode_ui()
elif service == "deezer":
if region_label:
region_label.hide()
self.qobuz_region_dropdown.hide()
else: else:
if region_label: self.qobuz_mode_label.hide()
region_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() self.qobuz_region_dropdown.hide()
def save_url(self): def save_url(self):
@@ -1376,6 +1539,22 @@ class SpotiFLACGUI(QWidget):
self.settings.sync() self.settings.sync()
self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}") 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): def save_settings(self):
self.settings.setValue('output_path', self.output_dir.text().strip()) self.settings.setValue('output_path', self.output_dir.text().strip())
self.settings.sync() self.settings.sync()
@@ -1437,7 +1616,8 @@ class SpotiFLACGUI(QWidget):
track_number=1, track_number=1,
duration_ms=track_data.get("duration_ms", 0), duration_ms=track_data.get("duration_ms", 0),
id=track_id, id=track_id,
isrc=track_data.get("isrc", "") isrc=track_data.get("isrc", ""),
release_date=track_data.get("release_date", "")
) )
self.tracks = [track] self.tracks = [track]
@@ -1470,7 +1650,8 @@ class SpotiFLACGUI(QWidget):
track_number=track["track_number"], track_number=track["track_number"],
duration_ms=track.get("duration_ms", 0), duration_ms=track.get("duration_ms", 0),
id=track_id, id=track_id,
isrc=track.get("isrc", "") isrc=track.get("isrc", ""),
release_date=track.get("release_date", "")
)) ))
self.all_tracks = self.tracks.copy() self.all_tracks = self.tracks.copy()
@@ -1501,7 +1682,8 @@ class SpotiFLACGUI(QWidget):
track_number=track.get("track_number", len(self.tracks) + 1), track_number=track.get("track_number", len(self.tracks) + 1),
duration_ms=track.get("duration_ms", 0), duration_ms=track.get("duration_ms", 0),
id=track_id, id=track_id,
isrc=track.get("isrc", "") isrc=track.get("isrc", ""),
release_date=track.get("release_date", "")
)) ))
self.all_tracks = self.tracks.copy() self.all_tracks = self.tracks.copy()
@@ -1674,6 +1856,7 @@ class SpotiFLACGUI(QWidget):
def start_download_worker(self, tracks_to_download, outpath): def start_download_worker(self, tracks_to_download, outpath):
service = self.service_dropdown.currentData() service = self.service_dropdown.currentData()
qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us" 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( self.worker = DownloadWorker(
tracks_to_download, tracks_to_download,
@@ -1687,7 +1870,8 @@ class SpotiFLACGUI(QWidget):
self.use_artist_subfolders, self.use_artist_subfolders,
self.use_album_subfolders, self.use_album_subfolders,
service, service,
qobuz_region qobuz_region,
qobuz_mode
) )
self.worker.finished.connect(self.on_download_finished) self.worker.finished.connect(self.on_download_finished)
self.worker.progress.connect(self.update_progress) self.worker.progress.connect(self.update_progress)
@@ -1773,9 +1957,7 @@ class SpotiFLACGUI(QWidget):
if track in self.all_tracks: if track in self.all_tracks:
self.all_tracks.remove(track) 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() self.update_track_list_display()
+14 -8
View File
@@ -5,9 +5,13 @@ import re
import base64 import base64
import urllib3 import urllib3
from urllib.parse import unquote from urllib.parse import unquote
from random import randrange
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 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): def extract_data(html, patterns):
for pattern in patterns: for pattern in patterns:
if match := re.search(pattern, html): if match := re.search(pattern, html):
@@ -17,7 +21,7 @@ def extract_data(html, patterns):
def download_track(track_id, service="amazon", output_dir="."): def download_track(track_id, service="amazon", output_dir="."):
client = requests.Session() client = requests.Session()
client.verify = False 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: try:
spotify_url = f"https://open.spotify.com/track/{track_id}" 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 decoded_token = token
clean_url = url.replace('\\/', '/') clean_url = url.replace('\\/', '/')
print(f"Starting download for: {clean_url}") print(f"Fetching: {clean_url}")
request_data = { request_data = {
"account": {"id": "auto", "type": "country"}, "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')}") raise Exception(f"Request failed: {data.get('error', 'Unknown error')}")
completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}" completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}"
print("Processing track...") print("Fetching URL...")
while True: while True:
resp = client.get(completion_url, headers=headers).json() resp = client.get(completion_url, headers=headers).json()
if resp["status"] == "completed": if resp["status"] == "completed":
print("Processing completed!") print("URL found")
break break
elif resp["status"] == "error": elif resp["status"] == "error":
raise Exception(f"Processing failed: {resp.get('message', 'Unknown error')}") raise Exception(f"Processing failed: {resp.get('message', 'Unknown error')}")
elif progress := resp.get("progress"): elif progress := resp.get("progress"):
percent = int((progress.get("current", 0) / progress.get("total", 100)) * 100) percent = int((progress.get("current", 0) / progress.get("total", 100)) * 100)
print(f"Progress: {percent}%") print(f"\r{percent}%", end="")
time.sleep(1) time.sleep(1)
download_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}/download" 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_name = file_name.strip()
file_path = os.path.join(output_dir, file_name) file_path = os.path.join(output_dir, file_name)
print(f"Downloading: {file_name}") print(f"Downloading...")
with open(file_path, 'wb') as f: with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
if chunk: if chunk:
f.write(chunk) f.write(chunk)
print(f"Download completed: {file_path}") print("Download complete")
print("Done")
return file_path return file_path
except Exception as e: except Exception as e:
@@ -116,7 +121,8 @@ class LucidaDownloader:
raise Exception(f"Amazon Music download failed: {str(e)}") raise Exception(f"Amazon Music download failed: {str(e)}")
if __name__ == "__main__": if __name__ == "__main__":
print("=== AmazonDL - Amazon Music Downloader ===")
track_id = "2plbrEY59IikOBgBGLjaoe" track_id = "2plbrEY59IikOBgBGLjaoe"
service = "amazon" service = "amazon"
download_track(track_id, service) download_track(track_id, service)
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-au" viewBox="0 0 640 480">
<path fill="#00008B" d="M0 0h640v480H0z"/>
<path fill="#fff" d="m37.5 0 122 90.5L281 0h39v31l-120 89.5 120 89V240h-40l-120-89.5L40.5 240H0v-30l119.5-89L0 32V0z"/>
<path fill="red" d="M212 140.5 320 220v20l-135.5-99.5zm-92 10 3 17.5-96 72H0zM320 0v1.5l-124.5 94 1-22L295 0zM0 0l119.5 88h-30L0 21z"/>
<path fill="#fff" d="M120.5 0v240h80V0zM0 80v80h320V80z"/>
<path fill="red" d="M0 96.5v48h320v-48zM136.5 0v240h48V0z"/>
<path fill="#fff" d="m527 396.7-20.5 2.6 2.2 20.5-14.8-14.4-14.7 14.5 2-20.5-20.5-2.4 17.3-11.2-10.9-17.5 19.6 6.5 6.9-19.5 7.1 19.4 19.5-6.7-10.7 17.6zm-3.7-117.2 2.7-13-9.8-9 13.2-1.5 5.5-12.1 5.5 12.1 13.2 1.5-9.8 9 2.7 13-11.6-6.6zm-104.1-60-20.3 2.2 1.8 20.3-14.4-14.5-14.8 14.1 2.4-20.3-20.2-2.7 17.3-10.8-10.5-17.5 19.3 6.8L387 178l6.7 19.3 19.4-6.3-10.9 17.3 17.1 11.2ZM623 186.7l-20.9 2.7 2.3 20.9-15.1-14.7-15 14.8 2.1-21-20.9-2.4 17.7-11.5-11.1-17.9 20 6.7 7-19.8 7.2 19.8 19.9-6.9-11 18zm-96.1-83.5-20.7 2.3 1.9 20.8-14.7-14.8-15.1 14.4 2.4-20.7-20.7-2.8 17.7-11L467 73.5l19.7 6.9 7.3-19.5 6.8 19.7 19.8-6.5-11.1 17.6zM234 385.7l-45.8 5.4 4.6 45.9-32.8-32.4-33 32.2 4.9-45.9-45.8-5.8 38.9-24.8-24-39.4 43.6 15 15.8-43.4 15.5 43.5 43.7-14.7-24.3 39.2 38.8 25.1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+45
View File
@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-br" viewBox="0 0 640 480">
<g stroke-width="1pt">
<path fill="#229e45" fill-rule="evenodd" d="M0 0h640v480H0z"/>
<path fill="#f8e509" fill-rule="evenodd" d="m321.4 436 301.5-195.7L319.6 44 17.1 240.7z"/>
<path fill="#2b49a3" fill-rule="evenodd" d="M452.8 240c0 70.3-57.1 127.3-127.6 127.3A127.4 127.4 0 1 1 452.8 240"/>
<path fill="#ffffef" fill-rule="evenodd" d="m283.3 316.3-4-2.3-4 2 .9-4.5-3.2-3.4 4.5-.5 2.2-4 1.9 4.2 4.4.8-3.3 3m86 26.3-3.9-2.3-4 2 .8-4.5-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.1m-36.2-30-3.4-2-3.5 1.8.8-3.9-2.8-2.9 4-.4 1.8-3.6 1.6 3.7 3.9.7-3 2.7m87-8.5-3.4-2-3.5 1.8.8-3.9-2.7-2.8 3.9-.4 1.8-3.5 1.6 3.6 3.8.7-2.9 2.6m-87.3-22-4-2.2-4 2 .8-4.6-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.2m-104.6-35-4-2.2-4 2 1-4.6-3.3-3.3 4.6-.5 2-4.1 2 4.2 4.4.8-3.3 3.1m13.3 57.2-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.1-4 2 4.2 4.4.8-3.3 3.1m132-67.3-3.6-2-3.6 1.8.8-4-2.8-3 4-.5 1.9-3.6 1.7 3.8 4 .7-3 2.7m-6.7 38.3-2.7-1.6-2.9 1.4.6-3.2-2.2-2.3 3.2-.4 1.5-2.8 1.3 3 3 .5-2.2 2.2m-142.2 50.4-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2M419 299.8l-2.2-1.1-2.2 1 .5-2.3-1.7-1.6 2.4-.3 1.2-2 1 2 2.5.5-1.9 1.5"/>
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2"/>
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m42.3 3-2.6-1.4-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .5-2.3 2.1m-4.8 17-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m87.4-22.2-2.6-1.6-2.8 1.4.6-3-2-2.3 3-.3 1.4-2.7 1.2 2.8 3 .5-2.2 2.1m-25.1 3-2.7-1.5-2.7 1.4.6-3-2-2.3 3-.3 1.4-2.8 1.2 2.9 3 .5-2.2 2.1m-68.8-5.8-1.7-1-1.7.8.4-1.9-1.3-1.4 1.9-.2.8-1.7.8 1.8 1.9.3-1.4 1.3m167.8 45.4-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m-20.8 6-2.2-1.4-2.3 1.2.5-2.6-1.7-1.8 2.5-.3 1.2-2.3 1 2.4 2.5.4-1.9 1.8m10.4 2.3-2-1.2-2.1 1 .4-2.3-1.6-1.7 2.3-.3 1.1-2 1 2 2.3.5-1.7 1.6m29.1-22.8-2-1-2 1 .5-2.3-1.6-1.7 2.3-.3 1-2 1 2.1 2.1.4-1.6 1.6m-38.8 41.8-2.5-1.4-2.7 1.2.6-2.8-2-2 3-.3 1.3-2.5 1.2 2.6 3 .5-2.3 1.9m.6 14.2-2.4-1.4-2.4 1.3.6-2.8-1.9-2 2.7-.4 1.2-2.5 1.1 2.6 2.7.5-2 2m-19-23.1-1.9-1.2-2 1 .4-2.2-1.5-1.7 2.2-.2 1-2 1 2 2.2.4-1.6 1.6m-17.8 2.3-2-1.2-2 1 .5-2.2-1.6-1.7 2.3-.2 1-2 1 2 2.1.4-1.6 1.6m-30.4-24.6-2-1.1-2 1 .5-2.3-1.6-1.6 2.2-.3 1-2 1 2 2.2.5-1.6 1.5m3.7 57-1.6-.9-1.8.9.4-2-1.3-1.4 1.9-.2.9-1.7.8 1.8 1.9.3-1.4 1.3m-46.2-86.6-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.2-4 1.9 4.2 4.4.8-3.3 3.1"/>
<path fill="#fff" fill-rule="evenodd" d="M444.4 285.8a125 125 0 0 0 5.8-19.8c-67.8-59.5-143.3-90-238.7-83.7a125 125 0 0 0-8.5 20.9c113-10.8 196 39.2 241.4 82.6"/>
<path fill="#309e3a" d="m414 252.4 2.3 1.3a3 3 0 0 0-.3 2.2 3 3 0 0 0 1.4 1.7q1 .8 2 .7.9 0 1.3-.7l.2-.9-.5-1-1.5-1.8a8 8 0 0 1-1.8-3 4 4 0 0 1 2-4.4 4 4 0 0 1 2.3-.2 7 7 0 0 1 2.6 1.2q2.1 1.5 2.6 3.2a4 4 0 0 1-.6 3.3l-2.4-1.5q.5-1 .2-1.7-.2-.8-1.2-1.4a3 3 0 0 0-1.8-.7 1 1 0 0 0-.9.5q-.3.4-.1 1 .2.8 1.6 2.2t2 2.5a4 4 0 0 1-.3 4.2 4 4 0 0 1-1.9 1.5 4 4 0 0 1-2.4.3q-1.3-.3-2.8-1.3-2.2-1.5-2.7-3.3a5 5 0 0 1 .6-4zm-11.6-7.6 2.5 1.3a3 3 0 0 0-.2 2.2 3 3 0 0 0 1.4 1.6q1.1.8 2 .6.9 0 1.3-.8l.2-.8q0-.5-.5-1l-1.6-1.8q-1.7-1.6-2-2.8a4 4 0 0 1 .4-3.1 4 4 0 0 1 1.6-1.4 4 4 0 0 1 2.2-.3 7 7 0 0 1 2.6 1q2.3 1.5 2.7 3.1a4 4 0 0 1-.4 3.4l-2.5-1.4q.5-1 .2-1.7-.4-1-1.3-1.4a3 3 0 0 0-1.9-.6 1 1 0 0 0-.8.5q-.3.4-.1 1 .3.8 1.7 2.2 1.5 1.5 2 2.4a4 4 0 0 1 0 4.2 4 4 0 0 1-1.8 1.6 4 4 0 0 1-2.4.3 8 8 0 0 1-2.9-1.1 6 6 0 0 1-2.8-3.2 5 5 0 0 1 .4-4m-14.2-3.8 7.3-12 8.8 5.5-1.2 2-6.4-4-1.6 2.7 6 3.7-1.3 2-6-3.7-2 3.3 6.7 4-1.2 2zm-20.7-17 1.1-2 5.4 2.7-2.5 5q-1.2.3-3 .2a9 9 0 0 1-3.3-1 8 8 0 0 1-3-2.6 6 6 0 0 1-1-3.5 9 9 0 0 1 1-3.7 8 8 0 0 1 2.6-3 6 6 0 0 1 3.6-1.1q1.4 0 3.2 1 2.4 1.1 3.1 2.8a5 5 0 0 1 .3 3.5l-2.7-.8a3 3 0 0 0-.2-2q-.4-.9-1.6-1.4a4 4 0 0 0-3.1-.3q-1.5.5-2.6 2.6t-.7 3.8a4 4 0 0 0 2 2.4q.8.5 1.7.5h1.8l.8-1.6zm-90.2-22.3 2-14 4.2.7 1.1 9.8 3.9-9 4.2.6-2 13.8-2.7-.4 1.7-10.9-4.4 10.5-2.7-.4-1.1-11.3-1.6 11zm-14.1-1.7 1.3-14 10.3 1-.2 2.4-7.5-.7-.3 3 7 .7-.3 2.4-7-.7-.3 3.8 7.8.7-.2 2.4z"/>
<g stroke-opacity=".5">
<path fill="#309e3a" d="M216.5 191.3q0-2.2.7-3.6a7 7 0 0 1 1.4-1.9 5 5 0 0 1 1.8-1.2q1.5-.5 3-.5 3.1.1 5 2a7 7 0 0 1 1.6 5.5q0 3.3-2 5.3a7 7 0 0 1-5 1.7 7 7 0 0 1-4.8-2 7 7 0 0 1-1.7-5.3"/>
<path fill="#f7ffff" d="M219.4 191.3q0 2.3 1 3.6t2.8 1.3a4 4 0 0 0 2.8-1.1q1-1.2 1.1-3.7.1-2.4-1-3.6a4 4 0 0 0-2.7-1.3 4 4 0 0 0-2.8 1.2q-1.1 1.2-1.2 3.6"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="m233 198.5.2-14h6q2.2 0 3.2.5 1 .3 1.6 1.3c.6 1 .6 1.4.6 2.3a4 4 0 0 1-1 2.6 5 5 0 0 1-2.7 1.2l1.5 1.2q.6.6 1.5 2.3l1.7 2.8h-3.4l-2-3.2-1.4-2-.9-.6-1.4-.2h-.6v5.8z"/>
<path fill="#fff" d="M236 190.5h2q2.1 0 2.6-.2.5-.1.8-.5.4-.6.3-1 0-.9-.4-1.2-.3-.4-1-.6h-2l-2.3-.1z"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="m249 185.2 5.2.3q1.7 0 2.6.3a5 5 0 0 1 2 1.4 6 6 0 0 1 1.2 2.4q.4 1.4.3 3.3a9 9 0 0 1-.5 3q-.6 1.5-1.7 2.4a5 5 0 0 1-2 1q-1 .3-2.5.2l-5.3-.3z"/>
<path fill="#fff" d="m251.7 187.7-.5 9.3h3.8q.8 0 1.2-.5.5-.4.8-1.3t.4-2.6l-.1-2.5a3 3 0 0 0-.8-1.4l-1.2-.7-2.3-.3z"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="m317.6 210.2 3.3-13.6 4.4 1 3.2 1q1.1.6 1.6 1.9t.2 2.8q-.3 1.2-1 2a4 4 0 0 1-3 1.4q-1 0-3-.5l-1.7-.5-1.2 5.2z"/>
<path fill="#fff" d="m323 199.6-.8 3.8 1.5.4q1.6.4 2.2.3a2 2 0 0 0 1.6-1.5q0-.7-.2-1.3a2 2 0 0 0-1-.9l-1.9-.5-1.3-.3z"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="m330.6 214.1 4.7-13.2 5.5 2q2.2.8 3 1.4.8.7 1 1.8c.2 1.1.2 1.5 0 2.3q-.6 1.5-1.8 2.2-1.2.6-3 .3.6.7 1 1.6l.8 2.7.6 3.1-3.1-1.1-1-3.6-.7-2.4-.6-.8q-.3-.4-1.3-.7l-.5-.2-2 5.6z"/>
<path fill="#fff" d="m336 207.4 1.9.7q2 .7 2.5.7t.9-.3q.5-.3.6-.9.3-.6 0-1.2l-.8-.9-2-.7-2-.7-1.2 3.3z"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="M347 213.6a9 9 0 0 1 1.7-3.2l1.8-1.5 2-.7q1.5-.1 3.1.4a7 7 0 0 1 4.2 3.3q1.2 2.4.2 5.7a7 7 0 0 1-3.4 4.5q-2.3 1.3-5.2.4a7 7 0 0 1-4.2-3.3 7 7 0 0 1-.2-5.6"/>
<path fill="#fff" d="M349.8 214.4q-.7 2.3 0 3.8c.7 1.5 1.2 1.6 2.3 2q1.5.5 3-.4 1.4-.8 2.1-3.2.8-2.2 0-3.7a4 4 0 0 0-2.2-2 4 4 0 0 0-3 .3q-1.5.8-2.2 3.2"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="m374.3 233.1 6.4-12.4 5.3 2.7a10 10 0 0 1 2.7 1.9q.8.7.8 1.9c0 1.2 0 1.5-.4 2.2a4 4 0 0 1-2 2q-1.5.4-3.1-.2.6 1 .8 1.7.3.9.4 2.8l.2 3.2-3-1.5-.4-3.7-.3-2.5-.5-1-1.2-.7-.5-.3-2.7 5.2z"/>
<path fill="#fff" d="m380.5 227.2 1.9 1q1.8 1 2.3 1t1-.2q.4-.2.7-.8t.2-1.2l-.7-1-1.8-1-2-1z"/>
</g>
<g stroke-opacity=".5">
<path fill="#309e3a" d="M426.1 258.7a9 9 0 0 1 2.5-2.6 7 7 0 0 1 2.2-.9 6 6 0 0 1 2.2 0q1.5.3 2.8 1.2a7 7 0 0 1 3 4.4q.4 2.6-1.4 5.5a7 7 0 0 1-4.5 3.3 7 7 0 0 1-5.2-1.1 7 7 0 0 1-3-4.4q-.4-2.7 1.4-5.4"/>
<path fill="#fff" d="M428.6 260.3q-1.4 2-1.1 3.6a4 4 0 0 0 1.6 2.5q1.5 1 3 .6t2.9-2.4q1.4-2.1 1.1-3.6t-1.6-2.6c-1.4-1.1-2-.8-3-.5q-1.5.3-3 2.4z"/>
</g>
<path fill="#309e3a" d="m301.8 204.5 2.3-9.8 7.2 1.7-.3 1.6-5.3-1.2-.5 2.2 4.9 1.1-.4 1.7-4.9-1.2-.6 2.7 5.5 1.3-.4 1.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

+5 -1
View File
@@ -3,12 +3,16 @@ import asyncio
import os import os
import sys import sys
from mutagen.flac import FLAC 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: class DeezerDownloader:
def __init__(self): def __init__(self):
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update({ 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 self.progress_callback = None
+1 -1
View File
@@ -58,7 +58,7 @@ playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
album_base_url = 'https://api.spotify.com/v1/albums/{}' album_base_url = 'https://api.spotify.com/v1/albums/{}'
track_base_url = 'https://api.spotify.com/v1/tracks/{}' track_base_url = 'https://api.spotify.com/v1/tracks/{}'
headers = { 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': 'application/json',
'Accept-Language': 'en-US,en;q=0.9', 'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br', 'Accept-Encoding': 'gzip, deflate, br',
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-jp" viewBox="0 0 640 480">
<defs>
<clipPath id="jp-a">
<path fill-opacity=".7" d="M-88 32h640v480H-88z"/>
</clipPath>
</defs>
<g fill-rule="evenodd" stroke-width="1pt" clip-path="url(#jp-a)" transform="translate(88 -32)">
<path fill="#fff" d="M-128 32h720v480h-720z"/>
<circle cx="523.1" cy="344.1" r="194.9" fill="#bc002d" transform="translate(-168.4 8.6)scale(.76554)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

+251
View File
@@ -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()
+9 -5
View File
@@ -5,6 +5,10 @@ import re
from datetime import datetime from datetime import datetime
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType 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: class ProgressCallback:
def __call__(self, current, total): def __call__(self, current, total):
@@ -16,16 +20,16 @@ class ProgressCallback:
class QobuzDownloader: class QobuzDownloader:
def __init__(self, region="us", timeout=30): def __init__(self, region="us", timeout=30):
if region not in ["eu", "us"]: if region not in ["us", "eu", "br", "jp", "au"]:
raise ValueError("Region must be either 'us' or 'eu'") raise ValueError("Region must be one of: 'us', 'eu', 'br', 'jp', 'au'")
self.region = region self.region = region
self.timeout = timeout self.timeout = timeout
self.session = requests.Session() self.session = requests.Session()
self.headers = { 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.download_chunk_size = 256 * 1024
self.progress_callback = ProgressCallback() self.progress_callback = ProgressCallback()
@@ -223,7 +227,7 @@ class QobuzDownloader:
raise Exception(f"Metadata error: {e}") raise Exception(f"Metadata error: {e}")
def main(): def main():
print("=== QobuzDL - Qobuz Downloader ===") print("=== QobuzDL - Qobuz Downloader (Region) ===")
downloader = QobuzDownloader(region="us") downloader = QobuzDownloader(region="us")
isrc = "USAT22409172" isrc = "USAT22409172"