Compare commits

...

8 Commits

Author SHA1 Message Date
afkarxyz 49a8de1b35 v4.5 2025-09-11 13:26:52 +07:00
afkarxyz cd2500d1df Update README.md 2025-09-11 13:24:17 +07:00
afkarxyz ea1372f1fe v4.4 2025-08-08 00:20:44 +07:00
afkarxyz 65fbb9a8e9 v4.4 2025-08-08 00:20:57 +07:00
afkarxyz de16d9e25d v4.4 2025-08-08 00:17:03 +07:00
afkarxyz 6dd19b563b v4.3 2025-08-06 20:24:58 +07:00
afkarxyz 303b76d1ec v4.3 2025-08-06 20:21:52 +07:00
afkarxyz dbcd49225d v4.2 2025-07-26 09:40:32 +07:00
12 changed files with 791 additions and 103 deletions
+4 -2
View File
@@ -3,10 +3,10 @@
![SpotiFLAC](https://github.com/user-attachments/assets/b4c4f403-edbd-4a71-b74b-c7d433d47d06)
<div align="center">
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Qobuz, Tidal & Deezer.
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Qobuz, Tidal, Deezer & Amazon Music.
</div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.1/SpotiFLAC.exe)
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.4/SpotiFLAC.exe)
## Screenshots
@@ -16,6 +16,8 @@
![image](https://github.com/user-attachments/assets/f604dc04-4ee6-4084-b314-0be7cd5d7ef9)
![image](https://github.com/user-attachments/assets/40264f32-f2cf-4e91-b09d-fb628d9771f7)
## Lossless Audio Check
![image](https://github.com/user-attachments/assets/d63b422d-0ea3-4307-850f-96c99d7eaa9a)
+328 -93
View File
@@ -20,9 +20,11 @@ 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
@dataclass
class Track:
@@ -34,6 +36,7 @@ class Track:
duration_ms: int
id: str
isrc: str = ""
release_date: str = ""
class MetadataFetchWorker(QThread):
finished = pyqtSignal(dict)
@@ -58,9 +61,10 @@ class MetadataFetchWorker(QThread):
class DownloadWorker(QThread):
finished = pyqtSignal(bool, str, list)
progress = pyqtSignal(str, int)
def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False,
album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True,
use_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
@@ -70,9 +74,11 @@ class DownloadWorker(QThread):
self.album_or_playlist_name = album_or_playlist_name
self.filename_format = filename_format
self.use_track_numbers = use_track_numbers
self.use_artist_subfolders = use_artist_subfolders
self.use_album_subfolders = use_album_subfolders
self.service = service
self.qobuz_region = qobuz_region
self.qobuz_mode = qobuz_mode
self.is_paused = False
self.is_stopped = False
self.failed_tracks = []
@@ -89,18 +95,21 @@ 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()
downloader = TidalDownloader()
elif self.service == "deezer":
downloader = DeezerDownloader()
elif self.service == "amazon":
downloader = LucidaDownloader()
else:
downloader = TidalDownloader()
def progress_update(current, total):
if total > 0:
percent = (current / total) * 100
self.progress.emit("", int(percent))
else:
if total <= 0:
self.progress.emit("Processing metadata...", 0)
downloader.set_progress_callback(progress_update)
@@ -114,19 +123,28 @@ class DownloadWorker(QThread):
self.msleep(100)
if self.is_stopped:
return
self.progress.emit(f"Starting download ({i+1}/{total_tracks}): {track.title} - {track.artists}",
int((i) / total_tracks * 100))
try:
if self.is_playlist and self.use_album_subfolders:
album_folder = re.sub(r'[<>:"/\\|?*]', '_', track.album)
track_outpath = os.path.join(self.outpath, album_folder)
if self.is_playlist:
track_outpath = self.outpath
if self.use_artist_subfolders:
artist_name = track.artists.split(', ')[0] if ', ' in track.artists else track.artists
artist_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', artist_name)
track_outpath = os.path.join(track_outpath, artist_folder)
if self.use_album_subfolders:
album_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', track.album)
track_outpath = os.path.join(track_outpath, album_folder)
os.makedirs(track_outpath, exist_ok=True)
else:
track_outpath = self.outpath
if (self.is_album or (self.is_playlist and self.use_album_subfolders)) and self.use_track_numbers:
if (self.is_album or self.is_playlist) and self.use_track_numbers:
new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}"
else:
new_filename = self.get_formatted_filename(track)
@@ -214,6 +232,21 @@ class DownloadWorker(QThread):
raise Exception("Downloaded file not found")
else:
raise Exception("Deezer download failed")
elif self.service == "amazon":
self.progress.emit(f"Downloading from Amazon Music: {track.title} - {track.artists}", 0)
is_paused_callback = lambda: self.is_paused
is_stopped_callback = lambda: self.is_stopped
downloaded_file = downloader.download(
track.id,
track_outpath,
is_paused_callback=is_paused_callback,
is_stopped_callback=is_stopped_callback
)
if not downloaded_file or not os.path.exists(downloaded_file):
raise Exception("Amazon Music download failed")
else:
track_id = track.id
self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0)
@@ -334,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)}")
@@ -359,6 +396,19 @@ class DeezerStatusChecker(QThread):
self.error.emit(f"Error checking Deezer status: {str(e)}")
self.status_updated.emit(False)
class AmazonStatusChecker(QThread):
status_updated = pyqtSignal(bool)
error = pyqtSignal(str)
def run(self):
try:
response = requests.get("https://lucida.to/api/load?url=%2Fapi%2Fcountries%3Fservice%3Damazon", timeout=5)
is_online = response.status_code == 200
self.status_updated.emit(is_online)
except Exception as e:
self.error.emit(f"Error checking Amazon Music status: {str(e)}")
self.status_updated.emit(False)
class StatusIndicatorDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
item_data = index.data(Qt.ItemDataRole.UserRole)
@@ -403,7 +453,16 @@ class ServiceComboBox(QComboBox):
self.deezer_status_timer = QTimer(self)
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status)
self.deezer_status_timer.start(60000)
self.deezer_status_timer.start(60000)
self.amazon_status_checker = AmazonStatusChecker()
self.amazon_status_checker.status_updated.connect(self.update_amazon_service_status)
self.amazon_status_checker.error.connect(lambda e: print(f"Amazon Music status check error: {e}"))
self.amazon_status_checker.start()
self.amazon_status_timer = QTimer(self)
self.amazon_status_timer.timeout.connect(self.refresh_amazon_status)
self.amazon_status_timer.start(60000)
def setup_items(self):
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -411,7 +470,8 @@ class ServiceComboBox(QComboBox):
self.services = [
{'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False},
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False},
{'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False}
{'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False},
{'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png', 'online': False}
]
for service in self.services:
@@ -467,6 +527,19 @@ class ServiceComboBox(QComboBox):
self.deezer_status_checker.error.connect(lambda e: print(f"Deezer status check error: {e}"))
self.deezer_status_checker.start()
def update_amazon_service_status(self, is_online):
self.update_service_status('amazon', is_online)
def refresh_amazon_status(self):
if hasattr(self, 'amazon_status_checker') and self.amazon_status_checker.isRunning():
self.amazon_status_checker.quit()
self.amazon_status_checker.wait()
self.amazon_status_checker = AmazonStatusChecker()
self.amazon_status_checker.status_updated.connect(self.update_amazon_service_status)
self.amazon_status_checker.error.connect(lambda e: print(f"Amazon Music status check error: {e}"))
self.amazon_status_checker.start()
def currentData(self, role=Qt.ItemDataRole.UserRole + 1):
return super().currentData(role)
@@ -506,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:
@@ -549,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
@@ -564,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget):
def __init__(self):
super().__init__()
self.current_version = "4.2"
self.current_version = "4.5"
self.tracks = []
self.all_tracks = []
self.reset_state()
@@ -575,11 +651,15 @@ class SpotiFLACGUI(QWidget):
self.filename_format = self.settings.value('filename_format', 'title_artist')
self.use_track_numbers = self.settings.value('use_track_numbers', False, type=bool)
self.use_artist_subfolders = self.settings.value('use_artist_subfolders', False, type=bool)
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
self.service = self.settings.value('service', 'tidal')
self.qobuz_region = self.settings.value('qobuz_region', 'us')
self.qobuz_mode = self.settings.value('qobuz_mode', 'auto')
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
self.current_theme_color = self.settings.value('theme_color', '#2196F3')
self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration')
self.date_format = self.settings.value('date_format', 'dd_mm_yyyy')
self.elapsed_time = QTime(0, 0, 0)
self.timer = QTimer(self)
@@ -598,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):
@@ -698,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")
@@ -798,7 +944,6 @@ class SpotiFLACGUI(QWidget):
self.search_input.textChanged.connect(self.filter_tracks)
self.search_input.setFixedWidth(250)
search_input_layout.addWidget(self.search_input)
search_layout.addLayout(search_input_layout)
@@ -900,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()
@@ -922,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()
@@ -964,24 +1159,34 @@ 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)
checkbox_layout = QHBoxLayout()
self.track_number_checkbox = QCheckBox('Add Track Numbers to Album Files')
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.track_number_checkbox.setChecked(self.use_track_numbers)
self.track_number_checkbox.toggled.connect(self.save_track_numbering)
checkbox_layout.addWidget(self.track_number_checkbox)
self.artist_subfolder_checkbox = QCheckBox('Artist Subfolder (Playlist)')
self.artist_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.artist_subfolder_checkbox.setChecked(self.use_artist_subfolders)
self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting)
checkbox_layout.addWidget(self.artist_subfolder_checkbox)
checkbox_layout.addSpacing(10)
self.album_subfolder_checkbox = QCheckBox('Create Album Subfolders for Playlist Downloads')
self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)')
self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.album_subfolder_checkbox.setChecked(self.use_album_subfolders)
self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting)
checkbox_layout.addWidget(self.album_subfolder_checkbox)
checkbox_layout.addSpacing(10)
self.track_number_checkbox = QCheckBox('Track Number')
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.track_number_checkbox.setChecked(self.use_track_numbers)
self.track_number_checkbox.toggled.connect(self.save_track_numbering)
checkbox_layout.addWidget(self.track_number_checkbox)
checkbox_layout.addStretch()
file_layout.addLayout(checkbox_layout)
@@ -990,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()
@@ -1007,17 +1213,27 @@ 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()
auth_layout.addLayout(service_fallback_layout)
@@ -1027,8 +1243,9 @@ class SpotiFLACGUI(QWidget):
self.tab_widget.addTab(settings_tab, "Settings")
self.set_combobox_value(self.service_dropdown, self.service)
self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region)
self.set_combobox_value(self.qobuz_mode_dropdown, self.qobuz_mode)
self.set_combobox_value(self.track_list_format_dropdown, self.track_list_format)
self.set_combobox_value(self.date_format_dropdown, self.date_format)
self.update_service_ui()
@@ -1040,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()
@@ -1237,8 +1454,7 @@ class SpotiFLACGUI(QWidget):
about_layout.addWidget(section_widget)
footer_label = QLabel(f"v{self.current_version} | July 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)
@@ -1253,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):
@@ -1293,6 +1521,12 @@ class SpotiFLACGUI(QWidget):
self.use_track_numbers = self.track_number_checkbox.isChecked()
self.settings.setValue('use_track_numbers', self.use_track_numbers)
self.settings.sync()
def save_artist_subfolder_setting(self):
self.use_artist_subfolders = self.artist_subfolder_checkbox.isChecked()
self.settings.setValue('use_artist_subfolders', self.use_artist_subfolders)
self.settings.sync()
def save_album_subfolder_setting(self):
self.use_album_subfolders = self.album_subfolder_checkbox.isChecked()
self.settings.setValue('use_album_subfolders', self.use_album_subfolders)
@@ -1305,7 +1539,21 @@ 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())
@@ -1368,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]
@@ -1401,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()
@@ -1429,10 +1679,11 @@ class SpotiFLACGUI(QWidget):
title=track["name"],
artists=track["artists"],
album=track["album_name"],
track_number=len(self.tracks) + 1,
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()
@@ -1568,7 +1819,7 @@ class SpotiFLACGUI(QWidget):
if self.is_single_track:
self.download_all()
else:
selected_items = self.track_list.selectedItems()
selected_items = self.track_list.selectedItems()
if not selected_items:
self.log_output.append('Warning: Please select tracks to download.')
return
@@ -1600,10 +1851,12 @@ class SpotiFLACGUI(QWidget):
try:
self.start_download_worker(tracks_to_download, outpath)
except Exception as e:
self.log_output.append(f"Error: An error occurred while starting the download: {str(e)}")
self.log_output.append(f"Error: An error occurred while starting the download: {str(e)}")
def start_download_worker(self, tracks_to_download, outpath):
service = self.service_dropdown.currentData()
qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us"
qobuz_mode = self.qobuz_mode_dropdown.currentData() if service == "qobuz" else "auto"
self.worker = DownloadWorker(
tracks_to_download,
@@ -1614,9 +1867,11 @@ class SpotiFLACGUI(QWidget):
self.album_or_playlist_name,
self.filename_format,
self.use_track_numbers,
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)
@@ -1641,27 +1896,9 @@ class SpotiFLACGUI(QWidget):
self.tab_widget.setCurrentWidget(self.process_tab)
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 "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)
else:
self.log_output.append(message)
else:
self.log_output.append(message)
if percentage > 0 and not "Download progress:" in message:
self.log_output.append(message)
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
if percentage > 0:
self.progress_bar.setValue(percentage)
def stop_download(self):
@@ -1720,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()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+128
View File
@@ -0,0 +1,128 @@
import requests
import time
import os
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):
return match.group(1)
return None
def download_track(track_id, service="amazon", output_dir="."):
client = requests.Session()
client.verify = False
headers = {'User-Agent': get_random_user_agent()}
try:
spotify_url = f"https://open.spotify.com/track/{track_id}"
params = {"url": spotify_url, "country": "auto", "to": service}
response = client.get("https://lucida.to", params=params, headers=headers, timeout=30)
html = response.text
token = extract_data(html, [r'token:"([^"]+)"', r'"token"\s*:\s*"([^"]+)"'])
url = extract_data(html, [r'"url":"([^"]+)"', r'url:"([^"]+)"'])
expiry = extract_data(html, [r'tokenExpiry:(\d+)', r'"tokenExpiry"\s*:\s*(\d+)'])
if not (token and url):
raise Exception("Could not extract required data")
try:
decoded_token = base64.b64decode(base64.b64decode(token).decode('latin1')).decode('latin1')
except:
decoded_token = token
clean_url = url.replace('\\/', '/')
print(f"Fetching: {clean_url}")
request_data = {
"account": {"id": "auto", "type": "country"},
"compat": "false", "downscale": "original", "handoff": True,
"metadata": True, "private": True,
"token": {"primary": decoded_token, "expiry": int(expiry) if expiry else None},
"upload": {"enabled": False, "service": "pixeldrain"},
"url": clean_url
}
response = client.post("https://lucida.to/api/load?url=/api/fetch/stream/v2",
json=request_data, headers=headers)
if csrf_token := response.cookies.get('csrf_token'):
headers['X-CSRF-Token'] = csrf_token
data = response.json()
if not data.get("success"):
raise Exception(f"Request failed: {data.get('error', 'Unknown error')}")
completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}"
print("Fetching URL...")
while True:
resp = client.get(completion_url, headers=headers).json()
if resp["status"] == "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"\r{percent}%", end="")
time.sleep(1)
download_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}/download"
response = client.get(download_url, stream=True, headers=headers)
file_name = "track.flac"
if content_disp := response.headers.get('content-disposition'):
if match := re.search(r'filename[*]?=([^;]+)', content_disp):
raw_name = match.group(1).strip('"\'')
file_name = unquote(raw_name[7:] if raw_name.startswith("UTF-8''") else raw_name)
for char in '<>:"/\\|?*':
file_name = file_name.replace(char, '')
file_name = file_name.strip()
file_path = os.path.join(output_dir, 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("Download complete")
print("Done")
return file_path
except Exception as e:
print(f"Error: {str(e)}")
return None
class LucidaDownloader:
def __init__(self):
self.progress_callback = None
def set_progress_callback(self, callback):
self.progress_callback = callback
def download(self, track_id, output_dir, is_paused_callback=None, is_stopped_callback=None):
try:
return download_track(track_id, service="amazon", output_dir=output_dir)
except Exception as e:
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)
+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 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
+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/{}'
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',
+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 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"
+1 -1
View File
@@ -1,3 +1,3 @@
{
"version": "4.1"
"version": "4.4"
}