Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a28e8bd94 | |||
| f75385c4e8 | |||
| 2eac274ee0 | |||
| 49a8de1b35 | |||
| cd2500d1df | |||
| ea1372f1fe | |||
| 65fbb9a8e9 |
@@ -3,10 +3,10 @@
|
||||

|
||||
|
||||
<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.3/SpotiFLAC.exe)
|
||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.5/SpotiFLAC.exe)
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Lossless Audio Check
|
||||
|
||||

|
||||
|
||||
+311
-49
@@ -20,7 +20,8 @@ from PyQt6.QtGui import QIcon, QTextCursor, QDesktopServices, QPixmap, QBrush
|
||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
|
||||
from getMetadata import get_filtered_data, parse_uri, SpotifyInvalidUrlException
|
||||
from qobuzDL import QobuzDownloader
|
||||
from qobuzAutoDL import QobuzDownloader as QobuzAutoDownloader
|
||||
from qobuzRegionDL import QobuzDownloader as QobuzRegionDownloader
|
||||
from tidalDL import TidalDownloader
|
||||
from deezerDL import DeezerDownloader
|
||||
from amazonDL import LucidaDownloader
|
||||
@@ -35,6 +36,7 @@ class Track:
|
||||
duration_ms: int
|
||||
id: str
|
||||
isrc: str = ""
|
||||
release_date: str = ""
|
||||
|
||||
class MetadataFetchWorker(QThread):
|
||||
finished = pyqtSignal(dict)
|
||||
@@ -62,7 +64,7 @@ class DownloadWorker(QThread):
|
||||
|
||||
def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False,
|
||||
album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True,
|
||||
use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us"):
|
||||
use_artist_subfolders=False, use_album_subfolders=False, service="tidal", qobuz_region="us", qobuz_mode="auto"):
|
||||
super().__init__()
|
||||
self.tracks = tracks
|
||||
self.outpath = outpath
|
||||
@@ -76,6 +78,7 @@ class DownloadWorker(QThread):
|
||||
self.use_album_subfolders = use_album_subfolders
|
||||
self.service = service
|
||||
self.qobuz_region = qobuz_region
|
||||
self.qobuz_mode = qobuz_mode
|
||||
self.is_paused = False
|
||||
self.is_stopped = False
|
||||
self.failed_tracks = []
|
||||
@@ -92,7 +95,10 @@ class DownloadWorker(QThread):
|
||||
def run(self):
|
||||
try:
|
||||
if self.service == "qobuz":
|
||||
downloader = QobuzDownloader(self.qobuz_region)
|
||||
if self.qobuz_mode == "auto":
|
||||
downloader = QobuzAutoDownloader()
|
||||
else:
|
||||
downloader = QobuzRegionDownloader(self.qobuz_region)
|
||||
elif self.service == "tidal":
|
||||
downloader = TidalDownloader()
|
||||
elif self.service == "deezer":
|
||||
@@ -350,7 +356,7 @@ class TidalStatusChecker(QThread):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
response = requests.get("https://hifi.401658.xyz", timeout=5)
|
||||
response = requests.get("https://tidal.401658.xyz", timeout=5)
|
||||
is_online = response.status_code == 200 or response.status_code == 429
|
||||
self.status_updated.emit(is_online)
|
||||
except Exception as e:
|
||||
@@ -361,13 +367,17 @@ class QobuzStatusChecker(QThread):
|
||||
status_updated = pyqtSignal(bool)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, region="us"):
|
||||
def __init__(self, region="us", mode="auto"):
|
||||
super().__init__()
|
||||
self.region = region
|
||||
self.mode = mode
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
response = requests.get(f"https://{self.region}.qobuz.squid.wtf", timeout=5)
|
||||
if self.mode == "auto":
|
||||
response = requests.get("https://qobuz.squid.wtf", timeout=5)
|
||||
else:
|
||||
response = requests.get(f"https://{self.region}.qqdl.site", timeout=5)
|
||||
self.status_updated.emit(response.status_code == 200)
|
||||
except Exception as e:
|
||||
self.error.emit(f"Error checking Qobuz status: {str(e)}")
|
||||
@@ -569,8 +579,11 @@ class QobuzRegionComboBox(QComboBox):
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
self.regions = [
|
||||
{'id': 'us', 'name': 'USA', 'icon': 'us.svg', 'online': False},
|
||||
{'id': 'eu', 'name': 'Europe', 'icon': 'eu.svg', 'online': False},
|
||||
{'id': 'us', 'name': 'North America', 'icon': 'us.svg', 'online': False}
|
||||
{'id': 'br', 'name': 'Brazil', 'icon': 'br.svg', 'online': False},
|
||||
{'id': 'jp', 'name': 'Japan', 'icon': 'jp.svg', 'online': False},
|
||||
{'id': 'au', 'name': 'Australia', 'icon': 'au.svg', 'online': False}
|
||||
]
|
||||
|
||||
for region in self.regions:
|
||||
@@ -612,7 +625,7 @@ class QobuzRegionComboBox(QComboBox):
|
||||
|
||||
for region in self.regions:
|
||||
region_id = region['id']
|
||||
checker = QobuzStatusChecker(region_id)
|
||||
checker = QobuzStatusChecker(region_id, "region")
|
||||
checker.status_updated.connect(lambda status, rid=region_id: self.handle_status_update(rid, status))
|
||||
checker.start()
|
||||
self.status_checkers[region_id] = checker
|
||||
@@ -627,7 +640,7 @@ class QobuzRegionComboBox(QComboBox):
|
||||
class SpotiFLACGUI(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.current_version = "4.4"
|
||||
self.current_version = "4.6"
|
||||
self.tracks = []
|
||||
self.all_tracks = []
|
||||
self.reset_state()
|
||||
@@ -642,8 +655,11 @@ class SpotiFLACGUI(QWidget):
|
||||
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
|
||||
self.service = self.settings.value('service', 'tidal')
|
||||
self.qobuz_region = self.settings.value('qobuz_region', 'us')
|
||||
self.qobuz_mode = self.settings.value('qobuz_mode', 'auto')
|
||||
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
|
||||
self.current_theme_color = self.settings.value('theme_color', '#2196F3')
|
||||
self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration')
|
||||
self.date_format = self.settings.value('date_format', 'dd_mm_yyyy')
|
||||
|
||||
self.elapsed_time = QTime(0, 0, 0)
|
||||
self.timer = QTimer(self)
|
||||
@@ -662,6 +678,9 @@ class SpotiFLACGUI(QWidget):
|
||||
if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value:
|
||||
combobox.setCurrentIndex(i)
|
||||
return True
|
||||
if combobox.itemData(i, Qt.ItemDataRole.UserRole) == target_value:
|
||||
combobox.setCurrentIndex(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_updates(self):
|
||||
@@ -762,11 +781,74 @@ class SpotiFLACGUI(QWidget):
|
||||
|
||||
self.update_track_list_display()
|
||||
|
||||
def format_track_date(self, release_date):
|
||||
if not release_date:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if len(release_date) == 4:
|
||||
date_obj = datetime.strptime(release_date, "%Y")
|
||||
if self.date_format == "yyyy":
|
||||
return date_obj.strftime('%Y')
|
||||
else:
|
||||
return date_obj.strftime('%Y')
|
||||
elif len(release_date) == 7:
|
||||
date_obj = datetime.strptime(release_date, "%Y-%m")
|
||||
if self.date_format == "dd_mm_yyyy":
|
||||
return date_obj.strftime('%m-%Y')
|
||||
elif self.date_format == "yyyy_mm_dd":
|
||||
return date_obj.strftime('%Y-%m')
|
||||
else:
|
||||
return date_obj.strftime('%Y')
|
||||
else:
|
||||
date_obj = datetime.strptime(release_date, "%Y-%m-%d")
|
||||
if self.date_format == "dd_mm_yyyy":
|
||||
return date_obj.strftime('%d-%m-%Y')
|
||||
elif self.date_format == "yyyy_mm_dd":
|
||||
return date_obj.strftime('%Y-%m-%d')
|
||||
else:
|
||||
return date_obj.strftime('%Y')
|
||||
except ValueError:
|
||||
return release_date
|
||||
|
||||
def update_track_list_display(self):
|
||||
self.track_list.clear()
|
||||
for i, track in enumerate(self.tracks, 1):
|
||||
duration = self.format_duration(track.duration_ms)
|
||||
self.track_list.addItem(f"{i}. {track.title} - {track.artists} • {duration}")
|
||||
formatted_date = self.format_track_date(track.release_date)
|
||||
|
||||
if self.track_list_format == "artist_track_date_duration":
|
||||
display_parts = [f"{i}. {track.artists} - {track.title}"]
|
||||
if formatted_date:
|
||||
display_parts.append(formatted_date)
|
||||
display_parts.append(duration)
|
||||
display_text = " • ".join(display_parts)
|
||||
elif self.track_list_format == "track_artist_date":
|
||||
display_parts = [f"{i}. {track.title} - {track.artists}"]
|
||||
if formatted_date:
|
||||
display_parts.append(formatted_date)
|
||||
display_text = " • ".join(display_parts)
|
||||
elif self.track_list_format == "artist_track_date":
|
||||
display_parts = [f"{i}. {track.artists} - {track.title}"]
|
||||
if formatted_date:
|
||||
display_parts.append(formatted_date)
|
||||
display_text = " • ".join(display_parts)
|
||||
elif self.track_list_format == "track_artist_duration":
|
||||
display_text = f"{i}. {track.title} - {track.artists} • {duration}"
|
||||
elif self.track_list_format == "artist_track_duration":
|
||||
display_text = f"{i}. {track.artists} - {track.title} • {duration}"
|
||||
elif self.track_list_format == "track_artist":
|
||||
display_text = f"{i}. {track.title} - {track.artists}"
|
||||
elif self.track_list_format == "artist_track":
|
||||
display_text = f"{i}. {track.artists} - {track.title}"
|
||||
else:
|
||||
display_parts = [f"{i}. {track.title} - {track.artists}"]
|
||||
if formatted_date:
|
||||
display_parts.append(formatted_date)
|
||||
display_parts.append(duration)
|
||||
display_text = " • ".join(display_parts)
|
||||
|
||||
self.track_list.addItem(display_text)
|
||||
|
||||
def browse_output(self):
|
||||
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
|
||||
@@ -963,15 +1045,16 @@ class SpotiFLACGUI(QWidget):
|
||||
def setup_settings_tab(self):
|
||||
settings_tab = QWidget()
|
||||
settings_layout = QVBoxLayout()
|
||||
settings_layout.setSpacing(10)
|
||||
settings_layout.setContentsMargins(9, 9, 9, 9)
|
||||
settings_layout.setSpacing(4)
|
||||
settings_layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
output_group = QWidget()
|
||||
output_layout = QVBoxLayout(output_group)
|
||||
output_layout.setSpacing(5)
|
||||
output_layout.setSpacing(2)
|
||||
output_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
output_label = QLabel('Output Directory')
|
||||
output_label.setStyleSheet("font-weight: bold;")
|
||||
output_label.setStyleSheet("font-weight: bold; margin-top: 0px; margin-bottom: 5px;")
|
||||
output_layout.addWidget(output_label)
|
||||
|
||||
output_dir_layout = QHBoxLayout()
|
||||
@@ -985,18 +1068,67 @@ class SpotiFLACGUI(QWidget):
|
||||
self.output_browse.clicked.connect(self.browse_output)
|
||||
|
||||
output_dir_layout.addWidget(self.output_dir)
|
||||
output_dir_layout.addSpacing(5)
|
||||
output_dir_layout.addWidget(self.output_browse)
|
||||
|
||||
output_layout.addLayout(output_dir_layout)
|
||||
|
||||
settings_layout.addWidget(output_group)
|
||||
|
||||
dashboard_group = QWidget()
|
||||
dashboard_layout = QVBoxLayout(dashboard_group)
|
||||
dashboard_layout.setSpacing(3)
|
||||
dashboard_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
dashboard_label = QLabel('Dashboard Settings')
|
||||
dashboard_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
|
||||
dashboard_layout.addWidget(dashboard_label)
|
||||
|
||||
dashboard_controls_layout = QHBoxLayout()
|
||||
|
||||
list_format_label = QLabel('Track List View:')
|
||||
list_format_label.setFixedWidth(90)
|
||||
|
||||
self.track_list_format_dropdown = QComboBox()
|
||||
self.track_list_format_dropdown.addItem("Track - Artist - Date - Duration", "track_artist_date_duration")
|
||||
self.track_list_format_dropdown.addItem("Artist - Track - Date - Duration", "artist_track_date_duration")
|
||||
self.track_list_format_dropdown.addItem("Track - Artist - Date", "track_artist_date")
|
||||
self.track_list_format_dropdown.addItem("Artist - Track - Date", "artist_track_date")
|
||||
self.track_list_format_dropdown.addItem("Track - Artist - Duration", "track_artist_duration")
|
||||
self.track_list_format_dropdown.addItem("Artist - Track - Duration", "artist_track_duration")
|
||||
self.track_list_format_dropdown.addItem("Track - Artist", "track_artist")
|
||||
self.track_list_format_dropdown.addItem("Artist - Track", "artist_track")
|
||||
self.track_list_format_dropdown.currentIndexChanged.connect(self.save_track_list_format)
|
||||
|
||||
dashboard_controls_layout.addWidget(list_format_label)
|
||||
dashboard_controls_layout.addWidget(self.track_list_format_dropdown)
|
||||
|
||||
dashboard_controls_layout.addSpacing(15)
|
||||
|
||||
date_format_label = QLabel('Date Format:')
|
||||
date_format_label.setFixedWidth(80)
|
||||
|
||||
self.date_format_dropdown = QComboBox()
|
||||
self.date_format_dropdown.addItem("DD-MM-YYYY", "dd_mm_yyyy")
|
||||
self.date_format_dropdown.addItem("YYYY-MM-DD", "yyyy_mm_dd")
|
||||
self.date_format_dropdown.addItem("YYYY", "yyyy")
|
||||
self.date_format_dropdown.currentIndexChanged.connect(self.save_date_format)
|
||||
|
||||
dashboard_controls_layout.addWidget(date_format_label)
|
||||
dashboard_controls_layout.addWidget(self.date_format_dropdown)
|
||||
dashboard_controls_layout.addStretch()
|
||||
|
||||
dashboard_layout.addLayout(dashboard_controls_layout)
|
||||
|
||||
settings_layout.addWidget(dashboard_group)
|
||||
|
||||
file_group = QWidget()
|
||||
file_layout = QVBoxLayout(file_group)
|
||||
file_layout.setSpacing(5)
|
||||
file_layout.setSpacing(2)
|
||||
file_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
file_label = QLabel('File Settings')
|
||||
file_label.setStyleSheet("font-weight: bold;")
|
||||
file_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
|
||||
file_layout.addWidget(file_label)
|
||||
|
||||
format_layout = QHBoxLayout()
|
||||
@@ -1027,7 +1159,9 @@ class SpotiFLACGUI(QWidget):
|
||||
|
||||
format_layout.addWidget(format_label)
|
||||
format_layout.addWidget(self.title_artist_radio)
|
||||
format_layout.addSpacing(10)
|
||||
format_layout.addWidget(self.artist_title_radio)
|
||||
format_layout.addSpacing(10)
|
||||
format_layout.addWidget(self.title_only_radio)
|
||||
format_layout.addStretch()
|
||||
file_layout.addLayout(format_layout)
|
||||
@@ -1039,14 +1173,16 @@ class SpotiFLACGUI(QWidget):
|
||||
self.artist_subfolder_checkbox.setChecked(self.use_artist_subfolders)
|
||||
self.artist_subfolder_checkbox.toggled.connect(self.save_artist_subfolder_setting)
|
||||
checkbox_layout.addWidget(self.artist_subfolder_checkbox)
|
||||
checkbox_layout.addSpacing(10)
|
||||
|
||||
self.album_subfolder_checkbox = QCheckBox('Album Subfolder (Playlist)')
|
||||
self.album_subfolder_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.album_subfolder_checkbox.setChecked(self.use_album_subfolders)
|
||||
self.album_subfolder_checkbox.toggled.connect(self.save_album_subfolder_setting)
|
||||
checkbox_layout.addWidget(self.album_subfolder_checkbox)
|
||||
checkbox_layout.addSpacing(10)
|
||||
|
||||
self.track_number_checkbox = QCheckBox('Track Number for Album')
|
||||
self.track_number_checkbox = QCheckBox('Track Number')
|
||||
self.track_number_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.track_number_checkbox.setChecked(self.use_track_numbers)
|
||||
self.track_number_checkbox.toggled.connect(self.save_track_numbering)
|
||||
@@ -1059,10 +1195,11 @@ class SpotiFLACGUI(QWidget):
|
||||
|
||||
auth_group = QWidget()
|
||||
auth_layout = QVBoxLayout(auth_group)
|
||||
auth_layout.setSpacing(5)
|
||||
auth_layout.setSpacing(2)
|
||||
auth_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
auth_label = QLabel('Service Settings')
|
||||
auth_label.setStyleSheet("font-weight: bold;")
|
||||
auth_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
|
||||
auth_layout.addWidget(auth_label)
|
||||
|
||||
service_fallback_layout = QHBoxLayout()
|
||||
@@ -1076,13 +1213,25 @@ class SpotiFLACGUI(QWidget):
|
||||
|
||||
service_fallback_layout.addSpacing(10)
|
||||
|
||||
region_label = QLabel('Region:')
|
||||
self.qobuz_mode_label = QLabel('Mode:')
|
||||
self.qobuz_mode_dropdown = QComboBox()
|
||||
self.qobuz_mode_dropdown.addItem("Auto", "auto")
|
||||
self.qobuz_mode_dropdown.addItem("Region", "region")
|
||||
self.qobuz_mode_dropdown.currentIndexChanged.connect(self.on_qobuz_mode_changed)
|
||||
service_fallback_layout.addWidget(self.qobuz_mode_label)
|
||||
service_fallback_layout.addWidget(self.qobuz_mode_dropdown)
|
||||
|
||||
service_fallback_layout.addSpacing(10)
|
||||
|
||||
self.region_label = QLabel('Region:')
|
||||
self.qobuz_region_dropdown = QobuzRegionComboBox()
|
||||
self.qobuz_region_dropdown.currentIndexChanged.connect(self.save_qobuz_region_setting)
|
||||
service_fallback_layout.addWidget(region_label)
|
||||
service_fallback_layout.addWidget(self.region_label)
|
||||
service_fallback_layout.addWidget(self.qobuz_region_dropdown)
|
||||
|
||||
region_label.hide()
|
||||
self.qobuz_mode_label.hide()
|
||||
self.qobuz_mode_dropdown.hide()
|
||||
self.region_label.hide()
|
||||
self.qobuz_region_dropdown.hide()
|
||||
|
||||
service_fallback_layout.addStretch()
|
||||
@@ -1094,6 +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()
|
||||
|
||||
@@ -1105,7 +1257,7 @@ class SpotiFLACGUI(QWidget):
|
||||
theme_tab = QWidget()
|
||||
theme_layout = QVBoxLayout()
|
||||
theme_layout.setSpacing(8)
|
||||
theme_layout.setContentsMargins(15, 15, 15, 15)
|
||||
theme_layout.setContentsMargins(8, 15, 15, 15)
|
||||
|
||||
grid_layout = QVBoxLayout()
|
||||
|
||||
@@ -1302,8 +1454,7 @@ class SpotiFLACGUI(QWidget):
|
||||
|
||||
about_layout.addWidget(section_widget)
|
||||
|
||||
footer_label = QLabel(f"v{self.current_version} | August 2025")
|
||||
footer_label.setStyleSheet("font-size: 12px; margin-top: 20px;")
|
||||
footer_label = QLabel(f"v{self.current_version} | September 2025")
|
||||
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
about_tab.setLayout(about_layout)
|
||||
@@ -1318,26 +1469,38 @@ class SpotiFLACGUI(QWidget):
|
||||
self.update_service_ui()
|
||||
self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}")
|
||||
|
||||
def on_qobuz_mode_changed(self, index):
|
||||
mode = self.qobuz_mode_dropdown.currentData()
|
||||
self.qobuz_mode = mode
|
||||
self.settings.setValue('qobuz_mode', mode)
|
||||
self.settings.sync()
|
||||
|
||||
self.update_qobuz_mode_ui()
|
||||
self.log_output.append(f"Qobuz mode changed to: {self.qobuz_mode_dropdown.currentText()}")
|
||||
|
||||
def update_service_ui(self):
|
||||
service = self.service
|
||||
|
||||
region_label = None
|
||||
for widget in self.qobuz_region_dropdown.parentWidget().children():
|
||||
if isinstance(widget, QLabel) and widget.text() == "Region:":
|
||||
region_label = widget
|
||||
break
|
||||
|
||||
if service == "qobuz":
|
||||
if region_label:
|
||||
region_label.show()
|
||||
self.qobuz_region_dropdown.show()
|
||||
elif service == "deezer":
|
||||
if region_label:
|
||||
region_label.hide()
|
||||
self.qobuz_region_dropdown.hide()
|
||||
self.qobuz_mode_label.show()
|
||||
self.qobuz_mode_dropdown.show()
|
||||
self.update_qobuz_mode_ui()
|
||||
else:
|
||||
if region_label:
|
||||
region_label.hide()
|
||||
self.qobuz_mode_label.hide()
|
||||
self.qobuz_mode_dropdown.hide()
|
||||
self.region_label.hide()
|
||||
self.qobuz_region_dropdown.hide()
|
||||
|
||||
def update_qobuz_mode_ui(self):
|
||||
mode = self.qobuz_mode_dropdown.currentData()
|
||||
if mode is None:
|
||||
mode = self.qobuz_mode
|
||||
|
||||
if mode == "region":
|
||||
self.region_label.show()
|
||||
self.qobuz_region_dropdown.show()
|
||||
else:
|
||||
self.region_label.hide()
|
||||
self.qobuz_region_dropdown.hide()
|
||||
|
||||
def save_url(self):
|
||||
@@ -1376,6 +1539,22 @@ class SpotiFLACGUI(QWidget):
|
||||
self.settings.sync()
|
||||
self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}")
|
||||
|
||||
def save_track_list_format(self):
|
||||
format_value = self.track_list_format_dropdown.currentData()
|
||||
self.track_list_format = format_value
|
||||
self.settings.setValue('track_list_format', format_value)
|
||||
self.settings.sync()
|
||||
if self.tracks:
|
||||
self.update_track_list_display()
|
||||
|
||||
def save_date_format(self):
|
||||
format_value = self.date_format_dropdown.currentData()
|
||||
self.date_format = format_value
|
||||
self.settings.setValue('date_format', format_value)
|
||||
self.settings.sync()
|
||||
if self.tracks:
|
||||
self.update_track_list_display()
|
||||
|
||||
def save_settings(self):
|
||||
self.settings.setValue('output_path', self.output_dir.text().strip())
|
||||
self.settings.sync()
|
||||
@@ -1417,6 +1596,10 @@ class SpotiFLACGUI(QWidget):
|
||||
self.handle_album_metadata(metadata)
|
||||
elif url_info["type"] == "playlist":
|
||||
self.handle_playlist_metadata(metadata)
|
||||
elif url_info["type"] == "artist_discography":
|
||||
self.handle_discography_metadata(metadata)
|
||||
elif url_info["type"] == "artist":
|
||||
self.handle_artist_metadata(metadata)
|
||||
|
||||
self.update_button_states()
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
@@ -1437,7 +1620,8 @@ class SpotiFLACGUI(QWidget):
|
||||
track_number=1,
|
||||
duration_ms=track_data.get("duration_ms", 0),
|
||||
id=track_id,
|
||||
isrc=track_data.get("isrc", "")
|
||||
isrc=track_data.get("isrc", ""),
|
||||
release_date=track_data.get("release_date", "")
|
||||
)
|
||||
|
||||
self.tracks = [track]
|
||||
@@ -1470,7 +1654,8 @@ class SpotiFLACGUI(QWidget):
|
||||
track_number=track["track_number"],
|
||||
duration_ms=track.get("duration_ms", 0),
|
||||
id=track_id,
|
||||
isrc=track.get("isrc", "")
|
||||
isrc=track.get("isrc", ""),
|
||||
release_date=track.get("release_date", "")
|
||||
))
|
||||
|
||||
self.all_tracks = self.tracks.copy()
|
||||
@@ -1501,7 +1686,8 @@ class SpotiFLACGUI(QWidget):
|
||||
track_number=track.get("track_number", len(self.tracks) + 1),
|
||||
duration_ms=track.get("duration_ms", 0),
|
||||
id=track_id,
|
||||
isrc=track.get("isrc", "")
|
||||
isrc=track.get("isrc", ""),
|
||||
release_date=track.get("release_date", "")
|
||||
))
|
||||
|
||||
self.all_tracks = self.tracks.copy()
|
||||
@@ -1513,9 +1699,57 @@ class SpotiFLACGUI(QWidget):
|
||||
'artists': playlist_data["playlist_info"]["owner"]["display_name"],
|
||||
'cover': playlist_data["playlist_info"]["owner"]["images"],
|
||||
'followers': playlist_data["playlist_info"]["followers"]["total"],
|
||||
'total_tracks': playlist_data["playlist_info"]["tracks"]["total"] }
|
||||
'total_tracks': playlist_data["playlist_info"]["tracks"]["total"]
|
||||
}
|
||||
self.update_display_after_fetch(metadata)
|
||||
|
||||
def handle_discography_metadata(self, discography_data):
|
||||
artist_info = discography_data["artist_info"]
|
||||
self.album_or_playlist_name = f"{artist_info['name']} - Discography ({artist_info['discography_type'].title()})"
|
||||
self.tracks = []
|
||||
|
||||
for track in discography_data["track_list"]:
|
||||
track_id = track["external_urls"].split("/")[-1] if track.get("external_urls") else ""
|
||||
|
||||
self.tracks.append(Track(
|
||||
external_urls=track.get("external_urls", ""),
|
||||
title=track["name"],
|
||||
artists=track["artists"],
|
||||
album=track["album_name"],
|
||||
track_number=track.get("track_number", len(self.tracks) + 1),
|
||||
duration_ms=track.get("duration_ms", 0),
|
||||
id=track_id,
|
||||
isrc=track.get("isrc", ""),
|
||||
release_date=track.get("release_date", "")
|
||||
))
|
||||
|
||||
self.all_tracks = self.tracks.copy()
|
||||
self.is_playlist = True
|
||||
self.is_album = self.is_single_track = False
|
||||
|
||||
metadata = {
|
||||
'title': f"{artist_info['name']} - Discography",
|
||||
'artists': f"{artist_info['discography_type'].title()} • {artist_info['total_albums']} albums",
|
||||
'cover': artist_info["images"],
|
||||
'followers': artist_info.get("followers", 0),
|
||||
'total_tracks': len(self.tracks),
|
||||
'discography_type': artist_info['discography_type']
|
||||
}
|
||||
self.update_display_after_fetch(metadata)
|
||||
|
||||
def handle_artist_metadata(self, artist_data):
|
||||
self.reset_state()
|
||||
|
||||
metadata = {
|
||||
'title': artist_data["artist"]["name"],
|
||||
'artists': f"Followers: {artist_data['artist']['followers']:,}",
|
||||
'cover': artist_data["artist"]["images"],
|
||||
'followers': artist_data["artist"]["followers"],
|
||||
'genres': artist_data["artist"].get("genres", [])
|
||||
}
|
||||
|
||||
self.update_info_widget_artist_only(metadata)
|
||||
|
||||
def update_display_after_fetch(self, metadata):
|
||||
self.track_list.setVisible(not self.is_single_track)
|
||||
|
||||
@@ -1571,12 +1805,40 @@ class SpotiFLACGUI(QWidget):
|
||||
self.type_label.setText(f"<b>Album</b> • {total_tracks} tracks")
|
||||
elif self.is_playlist:
|
||||
total_tracks = metadata.get('total_tracks', 0)
|
||||
self.type_label.setText(f"<b>Playlist</b> • {total_tracks} tracks")
|
||||
if metadata.get('discography_type'):
|
||||
discography_type = metadata['discography_type'].title()
|
||||
self.type_label.setText(f"<b>Discography ({discography_type})</b> • {total_tracks} tracks")
|
||||
else:
|
||||
self.type_label.setText(f"<b>Playlist</b> • {total_tracks} tracks")
|
||||
|
||||
self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
|
||||
|
||||
self.info_widget.show()
|
||||
|
||||
def update_info_widget_artist_only(self, metadata):
|
||||
self.title_label.setText(metadata['title'])
|
||||
self.artists_label.setText(f"<b>Followers</b> {metadata['followers']:,}")
|
||||
|
||||
if metadata.get('genres'):
|
||||
genres_text = ", ".join(metadata['genres'][:3])
|
||||
if len(metadata['genres']) > 3:
|
||||
genres_text += f" (+{len(metadata['genres']) - 3} more)"
|
||||
self.followers_label.setText(f"<b>Genres</b> {genres_text}")
|
||||
self.followers_label.show()
|
||||
else:
|
||||
self.followers_label.hide()
|
||||
|
||||
self.release_date_label.hide()
|
||||
self.type_label.setText("<b>Artist Profile</b> • No tracks available for download")
|
||||
|
||||
self.network_manager.get(QNetworkRequest(QUrl(metadata['cover'])))
|
||||
|
||||
self.track_list.hide()
|
||||
self.search_widget.hide()
|
||||
self.hide_track_buttons()
|
||||
|
||||
self.info_widget.show()
|
||||
|
||||
def reset_info_widget(self):
|
||||
self.title_label.clear()
|
||||
self.artists_label.clear()
|
||||
@@ -1674,6 +1936,7 @@ class SpotiFLACGUI(QWidget):
|
||||
def start_download_worker(self, tracks_to_download, outpath):
|
||||
service = self.service_dropdown.currentData()
|
||||
qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us"
|
||||
qobuz_mode = self.qobuz_mode_dropdown.currentData() if service == "qobuz" else "auto"
|
||||
|
||||
self.worker = DownloadWorker(
|
||||
tracks_to_download,
|
||||
@@ -1687,7 +1950,8 @@ class SpotiFLACGUI(QWidget):
|
||||
self.use_artist_subfolders,
|
||||
self.use_album_subfolders,
|
||||
service,
|
||||
qobuz_region
|
||||
qobuz_region,
|
||||
qobuz_mode
|
||||
)
|
||||
self.worker.finished.connect(self.on_download_finished)
|
||||
self.worker.progress.connect(self.update_progress)
|
||||
@@ -1773,9 +2037,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()
|
||||
|
||||
|
||||
+13
-8
@@ -5,9 +5,13 @@ import re
|
||||
import base64
|
||||
import urllib3
|
||||
from urllib.parse import unquote
|
||||
from random import randrange
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def get_random_user_agent():
|
||||
return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
|
||||
|
||||
def extract_data(html, patterns):
|
||||
for pattern in patterns:
|
||||
if match := re.search(pattern, html):
|
||||
@@ -17,7 +21,7 @@ def extract_data(html, patterns):
|
||||
def download_track(track_id, service="amazon", output_dir="."):
|
||||
client = requests.Session()
|
||||
client.verify = False
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
||||
headers = {'User-Agent': get_random_user_agent()}
|
||||
|
||||
try:
|
||||
spotify_url = f"https://open.spotify.com/track/{track_id}"
|
||||
@@ -39,7 +43,7 @@ def download_track(track_id, service="amazon", output_dir="."):
|
||||
decoded_token = token
|
||||
|
||||
clean_url = url.replace('\\/', '/')
|
||||
print(f"Starting download for: {clean_url}")
|
||||
print(f"Fetching: {clean_url}")
|
||||
|
||||
request_data = {
|
||||
"account": {"id": "auto", "type": "country"},
|
||||
@@ -61,18 +65,18 @@ def download_track(track_id, service="amazon", output_dir="."):
|
||||
raise Exception(f"Request failed: {data.get('error', 'Unknown error')}")
|
||||
|
||||
completion_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}"
|
||||
print("Processing track...")
|
||||
print("Fetching URL...")
|
||||
|
||||
while True:
|
||||
resp = client.get(completion_url, headers=headers).json()
|
||||
if resp["status"] == "completed":
|
||||
print("Processing completed!")
|
||||
print("URL found")
|
||||
break
|
||||
elif resp["status"] == "error":
|
||||
raise Exception(f"Processing failed: {resp.get('message', 'Unknown error')}")
|
||||
elif progress := resp.get("progress"):
|
||||
percent = int((progress.get("current", 0) / progress.get("total", 100)) * 100)
|
||||
print(f"Progress: {percent}%")
|
||||
print(f"\r{percent}%", end="")
|
||||
time.sleep(1)
|
||||
|
||||
download_url = f"https://{data['server']}.lucida.to/api/fetch/request/{data['handoff']}/download"
|
||||
@@ -88,14 +92,15 @@ def download_track(track_id, service="amazon", output_dir="."):
|
||||
file_name = file_name.strip()
|
||||
|
||||
file_path = os.path.join(output_dir, file_name)
|
||||
print(f"Downloading: {file_name}")
|
||||
print(f"Downloading...")
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
print(f"Download completed: {file_path}")
|
||||
print("Download complete")
|
||||
print("Done")
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
@@ -110,13 +115,13 @@ class LucidaDownloader:
|
||||
self.progress_callback = callback
|
||||
|
||||
def download(self, track_id, output_dir, is_paused_callback=None, is_stopped_callback=None):
|
||||
"""Download track using Lucida service"""
|
||||
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"
|
||||
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
+222
-2
@@ -57,8 +57,10 @@ token_url = 'https://open.spotify.com/api/token'
|
||||
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/{}'
|
||||
artist_base_url = 'https://api.spotify.com/v1/artists/{}'
|
||||
artist_albums_url = 'https://api.spotify.com/v1/artists/{}/albums'
|
||||
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',
|
||||
@@ -97,11 +99,22 @@ def parse_uri(uri):
|
||||
if parts[1] == "embed":
|
||||
parts = parts[1:]
|
||||
|
||||
if len(parts) > 1 and parts[1].startswith("intl-"):
|
||||
parts = parts[1:]
|
||||
|
||||
l = len(parts)
|
||||
if l == 3 and parts[1] in ["album", "track", "playlist"]:
|
||||
if l == 3 and parts[1] in ["album", "track", "playlist", "artist"]:
|
||||
return {"type": parts[1], "id": parts[2]}
|
||||
if l == 5 and parts[3] == "playlist":
|
||||
return {"type": parts[3], "id": parts[4]}
|
||||
if l >= 4 and parts[1] == "artist" and len(parts) >= 4:
|
||||
if parts[3] == "discography":
|
||||
discography_type = "all"
|
||||
if len(parts) >= 5 and parts[4] in ["all", "album", "single", "compilation"]:
|
||||
discography_type = parts[4]
|
||||
return {"type": "artist_discography", "id": parts[2], "discography_type": discography_type}
|
||||
else:
|
||||
return {"type": "artist", "id": parts[2]}
|
||||
|
||||
raise SpotifyInvalidUrlException("ERROR: unable to determine Spotify URL type or type is unsupported.")
|
||||
|
||||
@@ -337,6 +350,69 @@ def get_raw_spotify_data(spotify_url, batch: bool = False, delay: float = 1.0):
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to get track data: {str(e)}"}
|
||||
|
||||
elif url_info["type"] == "artist_discography":
|
||||
try:
|
||||
artist_data = get_json_from_api(
|
||||
artist_base_url.format(url_info["id"]),
|
||||
access_token
|
||||
)
|
||||
if not artist_data:
|
||||
return {"error": "Failed to get artist data"}
|
||||
|
||||
discography_type = url_info.get("discography_type", "all")
|
||||
if discography_type == "all":
|
||||
include_groups = "album,single,compilation"
|
||||
else:
|
||||
include_groups = discography_type
|
||||
|
||||
albums = []
|
||||
albums_url = f'{artist_albums_url.format(url_info["id"])}?include_groups={include_groups}&limit=50'
|
||||
|
||||
if batch:
|
||||
albums, num_batches = fetch_tracks_in_batches(albums_url, access_token, 50, delay)
|
||||
raw_data = {
|
||||
"artist_info": artist_data,
|
||||
"albums": albums,
|
||||
"discography_type": discography_type,
|
||||
"_batch_count": num_batches,
|
||||
"_batch_enabled": True
|
||||
}
|
||||
else:
|
||||
while albums_url:
|
||||
album_data = get_json_from_api(albums_url, access_token)
|
||||
if not album_data:
|
||||
break
|
||||
|
||||
albums.extend(album_data['items'])
|
||||
albums_url = album_data.get('next')
|
||||
if albums_url and "&locale=" in albums_url:
|
||||
albums_url = albums_url.split("&locale=")[0]
|
||||
|
||||
raw_data = {
|
||||
"artist_info": artist_data,
|
||||
"albums": albums,
|
||||
"discography_type": discography_type,
|
||||
"_batch_enabled": False
|
||||
}
|
||||
|
||||
raw_data['_token'] = access_token
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to get artist discography data: {str(e)}"}
|
||||
|
||||
elif url_info["type"] == "artist":
|
||||
try:
|
||||
artist_data = get_json_from_api(
|
||||
artist_base_url.format(url_info["id"]),
|
||||
access_token
|
||||
)
|
||||
if not artist_data:
|
||||
return {"error": "Failed to get artist data"}
|
||||
|
||||
raw_data = artist_data
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to get artist data: {str(e)}"}
|
||||
|
||||
return raw_data
|
||||
|
||||
def format_track_data(track_data):
|
||||
@@ -466,6 +542,134 @@ def format_playlist_data(playlist_data):
|
||||
"track_list": track_list
|
||||
}
|
||||
|
||||
def format_artist_discography_data(discography_data):
|
||||
artist_info = discography_data.get('artist_info', {})
|
||||
albums = discography_data.get('albums', [])
|
||||
access_token = discography_data.get('_token', '')
|
||||
|
||||
artist_image = ''
|
||||
if artist_info.get('images'):
|
||||
artist_image = artist_info.get('images', [{}])[0].get('url', '')
|
||||
|
||||
formatted_artist_info = {
|
||||
"name": artist_info.get('name', ''),
|
||||
"followers": artist_info.get('followers', {}).get('total', 0),
|
||||
"genres": artist_info.get('genres', []),
|
||||
"images": artist_image,
|
||||
"external_urls": artist_info.get('external_urls', {}).get('spotify', ''),
|
||||
"discography_type": discography_data.get('discography_type', 'all'),
|
||||
"total_albums": len(albums)
|
||||
}
|
||||
|
||||
if discography_data.get('_batch_enabled', False):
|
||||
formatted_artist_info["batch"] = f"{discography_data.get('_batch_count', 1)}"
|
||||
|
||||
album_list = []
|
||||
all_tracks = []
|
||||
|
||||
for album in albums:
|
||||
album_image = ''
|
||||
if album.get('images'):
|
||||
album_image = album.get('images', [{}])[0].get('url', '')
|
||||
|
||||
album_artists = []
|
||||
for artist in album.get('artists', []):
|
||||
album_artists.append(artist['name'])
|
||||
|
||||
album_info = {
|
||||
"id": album.get('id', ''),
|
||||
"name": album.get('name', ''),
|
||||
"album_type": album.get('album_type', ''),
|
||||
"release_date": album.get('release_date', ''),
|
||||
"total_tracks": album.get('total_tracks', 0),
|
||||
"artists": ", ".join(album_artists),
|
||||
"images": album_image,
|
||||
"external_urls": album.get('external_urls', {}).get('spotify', '')
|
||||
}
|
||||
|
||||
album_list.append(album_info)
|
||||
|
||||
if access_token and album.get('id'):
|
||||
try:
|
||||
album_tracks_data = get_json_from_api(
|
||||
f'{album_base_url.format(album.get("id"))}/tracks?limit=50',
|
||||
access_token
|
||||
)
|
||||
|
||||
if album_tracks_data:
|
||||
tracks = []
|
||||
tracks_url = f'{album_base_url.format(album.get("id"))}/tracks?limit=50'
|
||||
|
||||
while tracks_url:
|
||||
track_data = get_json_from_api(tracks_url, access_token)
|
||||
if not track_data:
|
||||
break
|
||||
|
||||
tracks.extend(track_data['items'])
|
||||
tracks_url = track_data.get('next')
|
||||
if tracks_url and "&locale=" in tracks_url:
|
||||
tracks_url = tracks_url.split("&locale=")[0]
|
||||
|
||||
for track in tracks:
|
||||
track_artists = []
|
||||
for artist in track.get('artists', []):
|
||||
track_artists.append(artist['name'])
|
||||
|
||||
track_id = track.get('id', '')
|
||||
track_isrc = ''
|
||||
|
||||
if track_id:
|
||||
try:
|
||||
full_track_data = get_json_from_api(
|
||||
track_base_url.format(track_id),
|
||||
access_token
|
||||
)
|
||||
if full_track_data:
|
||||
track_isrc = full_track_data.get('external_ids', {}).get('isrc', '')
|
||||
except:
|
||||
pass
|
||||
|
||||
formatted_track = {
|
||||
"artists": ", ".join(track_artists),
|
||||
"name": track.get('name', ''),
|
||||
"album_name": album.get('name', ''),
|
||||
"album_type": album.get('album_type', ''),
|
||||
"duration_ms": track.get('duration_ms', 0),
|
||||
"images": album_image,
|
||||
"release_date": album.get('release_date', ''),
|
||||
"track_number": track.get('track_number', 0),
|
||||
"external_urls": track.get('external_urls', {}).get('spotify', ''),
|
||||
"isrc": track_isrc
|
||||
}
|
||||
|
||||
all_tracks.append(formatted_track)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting tracks for album {album.get('name', '')}: {str(e)}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"artist_info": formatted_artist_info,
|
||||
"album_list": album_list,
|
||||
"track_list": all_tracks
|
||||
}
|
||||
|
||||
def format_artist_data(artist_data):
|
||||
artist_image = ''
|
||||
if artist_data.get('images'):
|
||||
artist_image = artist_data.get('images', [{}])[0].get('url', '')
|
||||
|
||||
return {
|
||||
"artist": {
|
||||
"name": artist_data.get('name', ''),
|
||||
"followers": artist_data.get('followers', {}).get('total', 0),
|
||||
"genres": artist_data.get('genres', []),
|
||||
"images": artist_image,
|
||||
"external_urls": artist_data.get('external_urls', {}).get('spotify', ''),
|
||||
"popularity": artist_data.get('popularity', 0)
|
||||
}
|
||||
}
|
||||
|
||||
def process_spotify_data(raw_data, data_type):
|
||||
if not raw_data or "error" in raw_data:
|
||||
return {"error": "Invalid data provided"}
|
||||
@@ -477,6 +681,10 @@ def process_spotify_data(raw_data, data_type):
|
||||
return format_album_data(raw_data)
|
||||
elif data_type == "playlist":
|
||||
return format_playlist_data(raw_data)
|
||||
elif data_type == "artist_discography":
|
||||
return format_artist_discography_data(raw_data)
|
||||
elif data_type == "artist":
|
||||
return format_artist_data(raw_data)
|
||||
else:
|
||||
return {"error": "Invalid data type"}
|
||||
except Exception as e:
|
||||
@@ -495,11 +703,23 @@ if __name__ == '__main__':
|
||||
album = "https://open.spotify.com/album/6J84szYCnMfzEcvIcfWMFL"
|
||||
song = "https://open.spotify.com/track/7so0lgd0zP2Sbgs2d7a1SZ"
|
||||
|
||||
artist_discography_all = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/all"
|
||||
artist_discography_albums = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/album"
|
||||
artist_discography_singles = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/single"
|
||||
artist_discography_compilations = "https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C/discography/compilation"
|
||||
|
||||
print("=== Testing Artist Discography (All) ===")
|
||||
filtered_discography = get_filtered_data(artist_discography_all, batch=True, delay=0.1)
|
||||
print(json.dumps(filtered_discography, indent=2))
|
||||
|
||||
print("\n=== Testing Playlist ===")
|
||||
filtered_playlist = get_filtered_data(playlist, batch=True, delay=0.1)
|
||||
print(json.dumps(filtered_playlist, indent=2))
|
||||
|
||||
print("\n=== Testing Album ===")
|
||||
filtered_album = get_filtered_data(album)
|
||||
print(json.dumps(filtered_album, indent=2))
|
||||
|
||||
print("\n=== Testing Track ===")
|
||||
filtered_track = get_filtered_data(song)
|
||||
print(json.dumps(filtered_track, indent=2))
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
@@ -144,7 +144,7 @@ class TidalDownloader:
|
||||
|
||||
def get_download_url(self, track_id, quality="LOSSLESS"):
|
||||
print("Fetching URL...")
|
||||
download_api_url = f"https://hifi.401658.xyz/track/?id={track_id}&quality={quality}"
|
||||
download_api_url = f"https://tidal.401658.xyz/track/?id={track_id}&quality={quality}"
|
||||
|
||||
try:
|
||||
response = requests.get(download_api_url, timeout=self.timeout)
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "4.3"
|
||||
"version": "4.5"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user