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) ![SpotiFLAC](https://github.com/user-attachments/assets/b4c4f403-edbd-4a71-b74b-c7d433d47d06)
<div align="center"> <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> </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 ## Screenshots
@@ -16,6 +16,8 @@
![image](https://github.com/user-attachments/assets/f604dc04-4ee6-4084-b314-0be7cd5d7ef9) ![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 ## Lossless Audio Check
![image](https://github.com/user-attachments/assets/d63b422d-0ea3-4307-850f-96c99d7eaa9a) ![image](https://github.com/user-attachments/assets/d63b422d-0ea3-4307-850f-96c99d7eaa9a)
+322 -87
View File
@@ -20,9 +20,11 @@ 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
@dataclass @dataclass
class Track: class Track:
@@ -34,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)
@@ -58,9 +61,10 @@ class MetadataFetchWorker(QThread):
class DownloadWorker(QThread): class DownloadWorker(QThread):
finished = pyqtSignal(bool, str, list) finished = pyqtSignal(bool, str, list)
progress = pyqtSignal(str, int) progress = pyqtSignal(str, int)
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_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
@@ -70,9 +74,11 @@ class DownloadWorker(QThread):
self.album_or_playlist_name = album_or_playlist_name self.album_or_playlist_name = album_or_playlist_name
self.filename_format = filename_format self.filename_format = filename_format
self.use_track_numbers = use_track_numbers self.use_track_numbers = use_track_numbers
self.use_artist_subfolders = use_artist_subfolders
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 = []
@@ -89,18 +95,21 @@ 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":
downloader = DeezerDownloader() downloader = DeezerDownloader()
elif self.service == "amazon":
downloader = LucidaDownloader()
else: else:
downloader = TidalDownloader() downloader = TidalDownloader()
def progress_update(current, total): def progress_update(current, total):
if total > 0: if total <= 0:
percent = (current / total) * 100
self.progress.emit("", int(percent))
else:
self.progress.emit("Processing metadata...", 0) self.progress.emit("Processing metadata...", 0)
downloader.set_progress_callback(progress_update) downloader.set_progress_callback(progress_update)
@@ -119,14 +128,23 @@ class DownloadWorker(QThread):
int((i) / total_tracks * 100)) int((i) / total_tracks * 100))
try: try:
if self.is_playlist and self.use_album_subfolders: if self.is_playlist:
album_folder = re.sub(r'[<>:"/\\|?*]', '_', track.album) track_outpath = self.outpath
track_outpath = os.path.join(self.outpath, album_folder)
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) os.makedirs(track_outpath, exist_ok=True)
else: else:
track_outpath = self.outpath 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)}" new_filename = f"{track.track_number:02d} - {self.get_formatted_filename(track)}"
else: else:
new_filename = self.get_formatted_filename(track) new_filename = self.get_formatted_filename(track)
@@ -214,6 +232,21 @@ class DownloadWorker(QThread):
raise Exception("Downloaded file not found") raise Exception("Downloaded file not found")
else: else:
raise Exception("Deezer download failed") 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: else:
track_id = track.id track_id = track.id
self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0) 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) 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)}")
@@ -359,6 +396,19 @@ class DeezerStatusChecker(QThread):
self.error.emit(f"Error checking Deezer status: {str(e)}") self.error.emit(f"Error checking Deezer status: {str(e)}")
self.status_updated.emit(False) 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): class StatusIndicatorDelegate(QStyledItemDelegate):
def paint(self, painter, option, index): def paint(self, painter, option, index):
item_data = index.data(Qt.ItemDataRole.UserRole) item_data = index.data(Qt.ItemDataRole.UserRole)
@@ -405,13 +455,23 @@ class ServiceComboBox(QComboBox):
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status) 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): def setup_items(self):
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
self.services = [ self.services = [
{'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False}, {'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False},
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.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: 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.error.connect(lambda e: print(f"Deezer status check error: {e}"))
self.deezer_status_checker.start() 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): def currentData(self, role=Qt.ItemDataRole.UserRole + 1):
return super().currentData(role) return super().currentData(role)
@@ -506,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:
@@ -549,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
@@ -564,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.2" self.current_version = "4.5"
self.tracks = [] self.tracks = []
self.all_tracks = [] self.all_tracks = []
self.reset_state() self.reset_state()
@@ -575,11 +651,15 @@ class SpotiFLACGUI(QWidget):
self.filename_format = self.settings.value('filename_format', 'title_artist') 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_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.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)
@@ -598,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):
@@ -698,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")
@@ -798,7 +944,6 @@ class SpotiFLACGUI(QWidget):
self.search_input.textChanged.connect(self.filter_tracks) self.search_input.textChanged.connect(self.filter_tracks)
self.search_input.setFixedWidth(250) self.search_input.setFixedWidth(250)
search_input_layout.addWidget(self.search_input) search_input_layout.addWidget(self.search_input)
search_layout.addLayout(search_input_layout) search_layout.addLayout(search_input_layout)
@@ -900,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()
@@ -922,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()
@@ -964,24 +1159,34 @@ 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)
checkbox_layout = QHBoxLayout() checkbox_layout = QHBoxLayout()
self.track_number_checkbox = QCheckBox('Add Track Numbers to Album Files') self.artist_subfolder_checkbox = QCheckBox('Artist Subfolder (Playlist)')
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor) self.artist_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
self.track_number_checkbox.setChecked(self.use_track_numbers) self.artist_subfolder_checkbox.setChecked(self.use_artist_subfolders)
self.track_number_checkbox.toggled.connect(self.save_track_numbering) self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting)
checkbox_layout.addWidget(self.track_number_checkbox) 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.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')
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() checkbox_layout.addStretch()
file_layout.addLayout(checkbox_layout) file_layout.addLayout(checkbox_layout)
@@ -990,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()
@@ -1007,17 +1213,27 @@ 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()
auth_layout.addLayout(service_fallback_layout) auth_layout.addLayout(service_fallback_layout)
@@ -1027,8 +1243,9 @@ class SpotiFLACGUI(QWidget):
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()
@@ -1040,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()
@@ -1237,8 +1454,7 @@ class SpotiFLACGUI(QWidget):
about_layout.addWidget(section_widget) about_layout.addWidget(section_widget)
footer_label = QLabel(f"v{self.current_version} | July 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)
@@ -1253,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):
@@ -1293,6 +1521,12 @@ class SpotiFLACGUI(QWidget):
self.use_track_numbers = self.track_number_checkbox.isChecked() self.use_track_numbers = self.track_number_checkbox.isChecked()
self.settings.setValue('use_track_numbers', self.use_track_numbers) self.settings.setValue('use_track_numbers', self.use_track_numbers)
self.settings.sync() 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): def save_album_subfolder_setting(self):
self.use_album_subfolders = self.album_subfolder_checkbox.isChecked() self.use_album_subfolders = self.album_subfolder_checkbox.isChecked()
self.settings.setValue('use_album_subfolders', self.use_album_subfolders) self.settings.setValue('use_album_subfolders', self.use_album_subfolders)
@@ -1305,7 +1539,21 @@ 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())
@@ -1368,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]
@@ -1401,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()
@@ -1429,10 +1679,11 @@ class SpotiFLACGUI(QWidget):
title=track["name"], title=track["name"],
artists=track["artists"], artists=track["artists"],
album=track["album_name"], 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), 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()
@@ -1601,9 +1852,11 @@ class SpotiFLACGUI(QWidget):
self.start_download_worker(tracks_to_download, outpath) self.start_download_worker(tracks_to_download, outpath)
except Exception as e: 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): 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,
@@ -1614,9 +1867,11 @@ class SpotiFLACGUI(QWidget):
self.album_or_playlist_name, self.album_or_playlist_name,
self.filename_format, self.filename_format,
self.use_track_numbers, self.use_track_numbers,
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)
@@ -1641,27 +1896,9 @@ class SpotiFLACGUI(QWidget):
self.tab_widget.setCurrentWidget(self.process_tab) self.tab_widget.setCurrentWidget(self.process_tab)
def update_progress(self, message, percentage): def update_progress(self, message, percentage):
if "Download progress:" in message or "Processing metadata..." in message: self.log_output.append(message)
current_text = self.log_output.toPlainText() self.log_output.moveCursor(QTextCursor.MoveOperation.End)
if percentage > 0:
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.progress_bar.setValue(percentage) self.progress_bar.setValue(percentage)
def stop_download(self): def stop_download(self):
@@ -1720,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()
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 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"
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "4.1" "version": "4.4"
} }