Compare commits

...

4 Commits

Author SHA1 Message Date
afkarxyz bdc7717ef3 v4.2 2025-07-26 09:37:45 +07:00
afkarxyz 9a7c539418 Update README.md 2025-07-26 08:39:19 +07:00
afkarxyz 888ce2b61c v4.1 2025-07-24 06:35:18 +07:00
afkarxyz e2e1ab1cfa v4.1 2025-07-24 06:34:25 +07:00
14 changed files with 580 additions and 456 deletions
+4 -12
View File
@@ -6,23 +6,15 @@
<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.
</div> </div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.0/SpotiFLAC.exe) ### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.1/SpotiFLAC.exe)
#
> [!Important]
> - Requires **Google Chrome, Chromium, Microsoft Edge,** or **Brave** to use `Deezer`
> - If after **Cloudflare** verification nothing happens, use a `VPN`, your country is likely blocked by `corsproxy.io`
## Screenshots ## Screenshots
![image](https://github.com/user-attachments/assets/70a5dceb-3374-4255-8f6a-4afb5ee534b0) ![image](https://github.com/user-attachments/assets/180b8322-ce2d-4842-a5dd-ac4d7b7a5efa)
![image](https://github.com/user-attachments/assets/9f0d6aa5-456b-4a90-b48a-7e0c22819ebd) ![image](https://github.com/user-attachments/assets/3f84d53b-2da1-4488-986c-772b82832f2d)
![image](https://github.com/user-attachments/assets/be9a79b5-260e-4948-9f72-10c735210ab7) ![image](https://github.com/user-attachments/assets/f604dc04-4ee6-4084-b314-0be7cd5d7ef9)
![image](https://github.com/user-attachments/assets/1feec621-f8bf-4b2a-ae73-afcb1fb1deba)
## Lossless Audio Check ## Lossless Audio Check
+338 -119
View File
@@ -2,15 +2,17 @@ import sys
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path
import requests import requests
import re import re
import asyncio import asyncio
from packaging import version from packaging import version
import qdarktheme
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit,
QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton, QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton,
QAbstractItemView, QSpacerItem, QSizePolicy, QProgressBar, QCheckBox, QDialog, QAbstractItemView, QProgressBar, QCheckBox, QDialog,
QDialogButtonBox, QComboBox, QStyledItemDelegate QDialogButtonBox, QComboBox, QStyledItemDelegate
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize
@@ -82,7 +84,7 @@ class DownloadWorker(QThread):
filename = f"{track.title}.flac" filename = f"{track.title}.flac"
else: else:
filename = f"{track.title} - {track.artists}.flac" filename = f"{track.title} - {track.artists}.flac"
return re.sub(r'[<>:"/\\|?*]', '_', filename) return re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', filename)
def run(self): def run(self):
try: try:
@@ -94,16 +96,12 @@ class DownloadWorker(QThread):
downloader = DeezerDownloader() downloader = DeezerDownloader()
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 percent = (current / total) * 100
current_mb = current / (1024 * 1024) self.progress.emit("", int(percent))
total_mb = total / (1024 * 1024)
self.progress.emit(f"Download progress: {percent:.2f}% ({current_mb:.2f}MB/{total_mb:.2f}MB)",
int(percent))
else: else:
self.progress.emit(f"Processing metadata...", 0) self.progress.emit("Processing metadata...", 0)
downloader.set_progress_callback(progress_update) downloader.set_progress_callback(progress_update)
@@ -133,7 +131,7 @@ class DownloadWorker(QThread):
else: else:
new_filename = self.get_formatted_filename(track) new_filename = self.get_formatted_filename(track)
new_filename = re.sub(r'[<>:"/\\|?*]', '_', new_filename) new_filename = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', new_filename)
new_filepath = os.path.join(track_outpath, new_filename) new_filepath = os.path.join(track_outpath, new_filename)
if os.path.exists(new_filepath) and os.path.getsize(new_filepath) > 0: if os.path.exists(new_filepath) and os.path.getsize(new_filepath) > 0:
@@ -169,23 +167,14 @@ class DownloadWorker(QThread):
is_paused_callback = lambda: self.is_paused is_paused_callback = lambda: self.is_paused
is_stopped_callback = lambda: self.is_stopped is_stopped_callback = lambda: self.is_stopped
try: download_result_details = downloader.download(
loop = asyncio.get_event_loop()
if loop.is_closed():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
download_result_details = loop.run_until_complete(downloader.download(
query=f"{track.title} {track.artists}", query=f"{track.title} {track.artists}",
isrc=track.isrc, isrc=track.isrc,
output_dir=track_outpath, output_dir=track_outpath,
quality="LOSSLESS", quality="LOSSLESS",
is_paused_callback=is_paused_callback, is_paused_callback=is_paused_callback,
is_stopped_callback=is_stopped_callback is_stopped_callback=is_stopped_callback
)) )
if isinstance(download_result_details, str) and os.path.exists(download_result_details): if isinstance(download_result_details, str) and os.path.exists(download_result_details):
downloaded_file = download_result_details downloaded_file = download_result_details
@@ -208,16 +197,7 @@ class DownloadWorker(QThread):
self.progress.emit(f"Downloading from Deezer with ISRC: {track.isrc}", 0) self.progress.emit(f"Downloading from Deezer with ISRC: {track.isrc}", 0)
try: success = asyncio.run(downloader.download_by_isrc(track.isrc, track_outpath))
loop = asyncio.get_event_loop()
if loop.is_closed():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
success = loop.run_until_complete(downloader.download_by_isrc(track.isrc, track_outpath))
if success: if success:
safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '-', '_')).rstrip() safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '-', '_')).rstrip()
@@ -311,26 +291,20 @@ class DownloadWorker(QThread):
class UpdateDialog(QDialog): class UpdateDialog(QDialog):
def __init__(self, current_version, new_version, parent=None): def __init__(self, current_version, new_version, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Update Available") self.setWindowTitle("Update Now")
self.setFixedWidth(400) self.setFixedWidth(400)
self.setModal(True) self.setModal(True)
layout = QVBoxLayout() layout = QVBoxLayout()
message = QLabel(f"A new version of SpotiFLAC is available!\n\n" message = QLabel(f"SpotiFLAC v{new_version} Available!")
f"Current version: v{current_version}\n"
f"New version: v{new_version}")
message.setWordWrap(True) message.setWordWrap(True)
layout.addWidget(message) layout.addWidget(message)
self.disable_check = QCheckBox("Turn off update checking")
self.disable_check.setCursor(Qt.CursorShape.PointingHandCursor)
layout.addWidget(self.disable_check)
button_box = QDialogButtonBox() button_box = QDialogButtonBox()
self.update_button = QPushButton("Update") self.update_button = QPushButton("Check")
self.update_button.setCursor(Qt.CursorShape.PointingHandCursor) self.update_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.cancel_button = QPushButton("Cancel") self.cancel_button = QPushButton("Later")
self.cancel_button.setCursor(Qt.CursorShape.PointingHandCursor) self.cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
button_box.addButton(self.update_button, QDialogButtonBox.ButtonRole.AcceptRole) button_box.addButton(self.update_button, QDialogButtonBox.ButtonRole.AcceptRole)
@@ -342,8 +316,6 @@ class UpdateDialog(QDialog):
self.update_button.clicked.connect(self.accept) self.update_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject) self.cancel_button.clicked.connect(self.reject)
class TidalStatusChecker(QThread): class TidalStatusChecker(QThread):
status_updated = pyqtSignal(bool) status_updated = pyqtSignal(bool)
@@ -398,7 +370,7 @@ class StatusIndicatorDelegate(QStyledItemDelegate):
circle_size = 6 circle_size = 6
circle_y = option.rect.center().y() - circle_size // 2 circle_y = option.rect.center().y() - circle_size // 2
circle_x = option.rect.right() - circle_size - 10 circle_x = option.rect.right() - circle_size - 5
painter.save() painter.save()
painter.setPen(Qt.PenStyle.NoPen) painter.setPen(Qt.PenStyle.NoPen)
@@ -422,7 +394,7 @@ class ServiceComboBox(QComboBox):
self.tidal_status_timer = QTimer(self) self.tidal_status_timer = QTimer(self)
self.tidal_status_timer.timeout.connect(self.refresh_tidal_status) self.tidal_status_timer.timeout.connect(self.refresh_tidal_status)
self.tidal_status_timer.start(6000) self.tidal_status_timer.start(60000)
self.deezer_status_checker = DeezerStatusChecker() self.deezer_status_checker = DeezerStatusChecker()
self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status) self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status)
@@ -431,7 +403,7 @@ class ServiceComboBox(QComboBox):
self.deezer_status_timer = QTimer(self) self.deezer_status_timer = QTimer(self)
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(6000) self.deezer_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__))
@@ -458,10 +430,10 @@ class ServiceComboBox(QComboBox):
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)
pixmap.save(path) pixmap.save(path)
def update_tidal_service_status(self, is_online): def update_service_status(self, service_id, is_online):
for i in range(self.count()): for i in range(self.count()):
service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) current_service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1)
if service_id == 'tidal': if current_service_id == service_id:
service_data = self.itemData(i, Qt.ItemDataRole.UserRole) service_data = self.itemData(i, Qt.ItemDataRole.UserRole)
if isinstance(service_data, dict): if isinstance(service_data, dict):
service_data['online'] = is_online service_data['online'] = is_online
@@ -469,24 +441,27 @@ class ServiceComboBox(QComboBox):
break break
self.update() self.update()
def update_tidal_service_status(self, is_online):
self.update_service_status('tidal', is_online)
def refresh_tidal_status(self): def refresh_tidal_status(self):
if hasattr(self, 'tidal_status_checker') and self.tidal_status_checker.isRunning():
self.tidal_status_checker.quit()
self.tidal_status_checker.wait()
self.tidal_status_checker = TidalStatusChecker() self.tidal_status_checker = TidalStatusChecker()
self.tidal_status_checker.status_updated.connect(self.update_tidal_service_status) self.tidal_status_checker.status_updated.connect(self.update_tidal_service_status)
self.tidal_status_checker.error.connect(lambda e: print(f"Tidal status check error: {e}")) self.tidal_status_checker.error.connect(lambda e: print(f"Tidal status check error: {e}"))
self.tidal_status_checker.start() self.tidal_status_checker.start()
def update_deezer_service_status(self, is_online): def update_deezer_service_status(self, is_online):
for i in range(self.count()): self.update_service_status('deezer', is_online)
service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1)
if service_id == 'deezer':
service_data = self.itemData(i, Qt.ItemDataRole.UserRole)
if isinstance(service_data, dict):
service_data['online'] = is_online
self.setItemData(i, service_data, Qt.ItemDataRole.UserRole)
break
self.update()
def refresh_deezer_status(self): def refresh_deezer_status(self):
if hasattr(self, 'deezer_status_checker') and self.deezer_status_checker.isRunning():
self.deezer_status_checker.quit()
self.deezer_status_checker.wait()
self.deezer_status_checker = DeezerStatusChecker() self.deezer_status_checker = DeezerStatusChecker()
self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status) self.deezer_status_checker.status_updated.connect(self.update_deezer_service_status)
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}"))
@@ -525,7 +500,7 @@ class QobuzRegionComboBox(QComboBox):
self.status_timer = QTimer(self) self.status_timer = QTimer(self)
self.status_timer.timeout.connect(self.check_status) self.status_timer.timeout.connect(self.check_status)
self.status_timer.start(10000) self.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__))
@@ -566,6 +541,12 @@ class QobuzRegionComboBox(QComboBox):
self.update() self.update()
def check_status(self): def check_status(self):
for region_id, checker in self.status_checkers.items():
if checker.isRunning():
checker.quit()
checker.wait()
self.status_checkers.clear()
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)
@@ -583,13 +564,13 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget): class SpotiFLACGUI(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "4.1" self.current_version = "4.2"
self.tracks = [] self.tracks = []
self.all_tracks = [] self.all_tracks = []
self.reset_state() self.reset_state()
self.settings = QSettings('SpotiFLAC', 'Settings') self.settings = QSettings('SpotiFLAC', 'Settings')
self.last_output_path = self.settings.value('output_path', os.path.expanduser("~\\Music")) self.last_output_path = self.settings.value('output_path', str(Path.home() / "Music"))
self.last_url = self.settings.value('spotify_url', '') self.last_url = self.settings.value('spotify_url', '')
self.filename_format = self.settings.value('filename_format', 'title_artist') self.filename_format = self.settings.value('filename_format', 'title_artist')
@@ -598,6 +579,7 @@ class SpotiFLACGUI(QWidget):
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.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.elapsed_time = QTime(0, 0, 0) self.elapsed_time = QTime(0, 0, 0)
self.timer = QTimer(self) self.timer = QTimer(self)
@@ -611,6 +593,13 @@ class SpotiFLACGUI(QWidget):
if self.check_for_updates: if self.check_for_updates:
QTimer.singleShot(0, self.check_updates) QTimer.singleShot(0, self.check_updates)
def set_combobox_value(self, combobox, target_value):
for i in range(combobox.count()):
if combobox.itemData(i, Qt.ItemDataRole.UserRole + 1) == target_value:
combobox.setCurrentIndex(i)
return True
return False
def check_updates(self): def check_updates(self):
try: try:
response = requests.get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/version.json") response = requests.get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/version.json")
@@ -622,9 +611,6 @@ class SpotiFLACGUI(QWidget):
dialog = UpdateDialog(self.current_version, new_version, self) dialog = UpdateDialog(self.current_version, new_version, self)
result = dialog.exec() result = dialog.exec()
if dialog.disable_check.isChecked():
self.settings.setValue('check_for_updates', False)
self.check_for_updates = False
if result == QDialog.DialogCode.Accepted: if result == QDialog.DialogCode.Accepted:
QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases")) QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases"))
@@ -682,12 +668,13 @@ class SpotiFLACGUI(QWidget):
spotify_label.setFixedWidth(100) spotify_label.setFixedWidth(100)
self.spotify_url = QLineEdit() self.spotify_url = QLineEdit()
self.spotify_url.setPlaceholderText("Please enter the Spotify URL") self.spotify_url.setPlaceholderText("Enter Spotify URL")
self.spotify_url.setClearButtonEnabled(True) self.spotify_url.setClearButtonEnabled(True)
self.spotify_url.setText(self.last_url) self.spotify_url.setText(self.last_url)
self.spotify_url.textChanged.connect(self.save_url) self.spotify_url.textChanged.connect(self.save_url)
self.fetch_btn = QPushButton('Fetch') self.fetch_btn = QPushButton('Fetch')
self.fetch_btn.setFixedWidth(80)
self.fetch_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.fetch_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.fetch_btn.clicked.connect(self.fetch_tracks) self.fetch_btn.clicked.connect(self.fetch_tracks)
@@ -730,6 +717,7 @@ class SpotiFLACGUI(QWidget):
self.setup_dashboard_tab() self.setup_dashboard_tab()
self.setup_process_tab() self.setup_process_tab()
self.setup_settings_tab() self.setup_settings_tab()
self.setup_theme_tab()
self.setup_about_tab() self.setup_about_tab()
def setup_dashboard_tab(self): def setup_dashboard_tab(self):
@@ -745,6 +733,7 @@ class SpotiFLACGUI(QWidget):
self.setup_track_buttons() self.setup_track_buttons()
dashboard_layout.addLayout(self.btn_layout) dashboard_layout.addLayout(self.btn_layout)
dashboard_layout.addWidget(self.single_track_container)
dashboard_tab.setLayout(dashboard_layout) dashboard_tab.setLayout(dashboard_layout)
self.tab_widget.addTab(dashboard_tab, "Dashboard") self.tab_widget.addTab(dashboard_tab, "Dashboard")
@@ -814,7 +803,7 @@ class SpotiFLACGUI(QWidget):
search_layout.addLayout(search_input_layout) search_layout.addLayout(search_input_layout)
self.search_widget.setLayout(search_layout) self.search_widget.setLayout(search_layout)
self.search_widget.hide() self.search_widget.hide()
def setup_track_buttons(self): def setup_track_buttons(self):
self.btn_layout = QHBoxLayout() self.btn_layout = QHBoxLayout()
@@ -824,7 +813,7 @@ class SpotiFLACGUI(QWidget):
self.clear_btn = QPushButton('Clear') self.clear_btn = QPushButton('Clear')
for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
btn.setFixedWidth(150) btn.setMinimumWidth(120)
btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.download_selected_btn.clicked.connect(self.download_selected) self.download_selected_btn.clicked.connect(self.download_selected)
@@ -834,8 +823,29 @@ class SpotiFLACGUI(QWidget):
self.btn_layout.addStretch() self.btn_layout.addStretch()
for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
self.btn_layout.addWidget(btn) self.btn_layout.addWidget(btn, 1)
self.btn_layout.addStretch() self.btn_layout.addStretch()
self.single_track_container = QWidget()
single_track_layout = QHBoxLayout(self.single_track_container)
single_track_layout.setContentsMargins(0, 0, 0, 0)
self.single_download_btn = QPushButton('Download')
self.single_clear_btn = QPushButton('Clear')
for btn in [self.single_download_btn, self.single_clear_btn]:
btn.setFixedWidth(120)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.single_download_btn.clicked.connect(self.download_all)
self.single_clear_btn.clicked.connect(self.clear_tracks)
single_track_layout.addStretch()
single_track_layout.addWidget(self.single_download_btn)
single_track_layout.addWidget(self.single_clear_btn)
single_track_layout.addStretch()
self.single_track_container.hide()
def setup_process_tab(self): def setup_process_tab(self):
self.process_tab = QWidget() self.process_tab = QWidget()
@@ -862,13 +872,19 @@ class SpotiFLACGUI(QWidget):
self.stop_btn = QPushButton('Stop') self.stop_btn = QPushButton('Stop')
self.pause_resume_btn = QPushButton('Pause') self.pause_resume_btn = QPushButton('Pause')
self.stop_btn.setFixedWidth(120)
self.pause_resume_btn.setFixedWidth(120)
self.stop_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.stop_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.pause_resume_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.pause_resume_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.stop_btn.clicked.connect(self.stop_download) self.stop_btn.clicked.connect(self.stop_download)
self.pause_resume_btn.clicked.connect(self.toggle_pause_resume) self.pause_resume_btn.clicked.connect(self.toggle_pause_resume)
control_layout.addStretch()
control_layout.addWidget(self.stop_btn) control_layout.addWidget(self.stop_btn)
control_layout.addWidget(self.pause_resume_btn) control_layout.addWidget(self.pause_resume_btn)
control_layout.addStretch()
process_layout.addLayout(control_layout) process_layout.addLayout(control_layout)
@@ -901,6 +917,7 @@ class SpotiFLACGUI(QWidget):
self.output_dir.textChanged.connect(self.save_settings) self.output_dir.textChanged.connect(self.save_settings)
self.output_browse = QPushButton('Browse') self.output_browse = QPushButton('Browse')
self.output_browse.setFixedWidth(80)
self.output_browse.setCursor(Qt.CursorShape.PointingHandCursor) self.output_browse.setCursor(Qt.CursorShape.PointingHandCursor)
self.output_browse.clicked.connect(self.browse_output) self.output_browse.clicked.connect(self.browse_output)
@@ -1008,15 +1025,8 @@ class SpotiFLACGUI(QWidget):
settings_layout.addStretch() settings_layout.addStretch()
settings_tab.setLayout(settings_layout) settings_tab.setLayout(settings_layout)
self.tab_widget.addTab(settings_tab, "Settings") self.tab_widget.addTab(settings_tab, "Settings")
for i in range(self.service_dropdown.count()): self.set_combobox_value(self.service_dropdown, self.service)
if self.service_dropdown.itemData(i, Qt.ItemDataRole.UserRole + 1) == self.service: self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region)
self.service_dropdown.setCurrentIndex(i)
break
for i in range(self.qobuz_region_dropdown.count()):
if self.qobuz_region_dropdown.itemData(i, Qt.ItemDataRole.UserRole + 1) == self.qobuz_region:
self.qobuz_region_dropdown.setCurrentIndex(i)
break
@@ -1026,18 +1036,189 @@ class SpotiFLACGUI(QWidget):
lambda region_id, is_online: self.service_dropdown.update_qobuz_status(region_id, is_online) lambda region_id, is_online: self.service_dropdown.update_qobuz_status(region_id, is_online)
) )
def setup_theme_tab(self):
theme_tab = QWidget()
theme_layout = QVBoxLayout()
theme_layout.setSpacing(8)
theme_layout.setContentsMargins(15, 15, 15, 15)
grid_layout = QVBoxLayout()
self.color_buttons = {}
first_row_palettes = [
("Red", [
("#FFCDD2", "100"), ("#EF9A9A", "200"), ("#E57373", "300"), ("#EF5350", "400"), ("#F44336", "500"), ("#E53935", "600"), ("#D32F2F", "700"), ("#C62828", "800"), ("#B71C1C", "900"), ("#FF8A80", "A100"), ("#FF5252", "A200"), ("#FF1744", "A400"), ("#D50000", "A700")
]),
("Pink", [
("#F8BBD0", "100"), ("#F48FB1", "200"), ("#F06292", "300"), ("#EC407A", "400"), ("#E91E63", "500"), ("#D81B60", "600"), ("#C2185B", "700"), ("#AD1457", "800"), ("#880E4F", "900"), ("#FF80AB", "A100"), ("#FF4081", "A200"), ("#F50057", "A400"), ("#C51162", "A700")
]),
("Purple", [
("#E1BEE7", "100"), ("#CE93D8", "200"), ("#BA68C8", "300"), ("#AB47BC", "400"), ("#9C27B0", "500"), ("#8E24AA", "600"), ("#7B1FA2", "700"), ("#6A1B9A", "800"), ("#4A148C", "900"), ("#EA80FC", "A100"), ("#E040FB", "A200"), ("#D500F9", "A400"), ("#AA00FF", "A700")
])
]
second_row_palettes = [
("Deep Purple", [
("#D1C4E9", "100"), ("#B39DDB", "200"), ("#9575CD", "300"), ("#7E57C2", "400"), ("#673AB7", "500"), ("#5E35B1", "600"), ("#512DA8", "700"), ("#4527A0", "800"), ("#311B92", "900"), ("#B388FF", "A100"), ("#7C4DFF", "A200"), ("#651FFF", "A400"), ("#6200EA", "A700")
]),
("Indigo", [
("#C5CAE9", "100"), ("#9FA8DA", "200"), ("#7986CB", "300"), ("#5C6BC0", "400"), ("#3F51B5", "500"), ("#3949AB", "600"), ("#303F9F", "700"), ("#283593", "800"), ("#1A237E", "900"), ("#8C9EFF", "A100"), ("#536DFE", "A200"), ("#3D5AFE", "A400"), ("#304FFE", "A700")
]),
("Blue", [
("#BBDEFB", "100"), ("#90CAF9", "200"), ("#64B5F6", "300"), ("#42A5F5", "400"), ("#2196F3", "500"), ("#1E88E5", "600"), ("#1976D2", "700"), ("#1565C0", "800"), ("#0D47A1", "900"), ("#82B1FF", "A100"), ("#448AFF", "A200"), ("#2979FF", "A400"), ("#2962FF", "A700")
])
]
third_row_palettes = [
("Light Blue", [
("#B3E5FC", "100"), ("#81D4FA", "200"), ("#4FC3F7", "300"), ("#29B6F6", "400"), ("#03A9F4", "500"), ("#039BE5", "600"), ("#0288D1", "700"), ("#0277BD", "800"), ("#01579B", "900"), ("#80D8FF", "A100"), ("#40C4FF", "A200"), ("#00B0FF", "A400"), ("#0091EA", "A700")
]),
("Cyan", [
("#B2EBF2", "100"), ("#80DEEA", "200"), ("#4DD0E1", "300"), ("#26C6DA", "400"), ("#00BCD4", "500"), ("#00ACC1", "600"), ("#0097A7", "700"), ("#00838F", "800"), ("#006064", "900"), ("#84FFFF", "A100"), ("#18FFFF", "A200"), ("#00E5FF", "A400"), ("#00B8D4", "A700")
]),
("Teal", [
("#B2DFDB", "100"), ("#80CBC4", "200"), ("#4DB6AC", "300"), ("#26A69A", "400"), ("#009688", "500"), ("#00897B", "600"), ("#00796B", "700"), ("#00695C", "800"), ("#004D40", "900"), ("#A7FFEB", "A100"), ("#64FFDA", "A200"), ("#1DE9B6", "A400"), ("#00BFA5", "A700")
])
]
fourth_row_palettes = [
("Green", [
("#C8E6C9", "100"), ("#A5D6A7", "200"), ("#81C784", "300"), ("#66BB6A", "400"), ("#4CAF50", "500"), ("#43A047", "600"), ("#388E3C", "700"), ("#2E7D32", "800"), ("#1B5E20", "900"), ("#B9F6CA", "A100"), ("#69F0AE", "A200"), ("#00E676", "A400"), ("#00C853", "A700")
]),
("Light Green", [
("#DCEDC8", "100"), ("#C5E1A5", "200"), ("#AED581", "300"), ("#9CCC65", "400"), ("#8BC34A", "500"), ("#7CB342", "600"), ("#689F38", "700"), ("#558B2F", "800"), ("#33691E", "900"), ("#CCFF90", "A100"), ("#B2FF59", "A200"), ("#76FF03", "A400"), ("#64DD17", "A700")
]),
("Lime", [
("#F0F4C3", "100"), ("#E6EE9C", "200"), ("#DCE775", "300"), ("#D4E157", "400"), ("#CDDC39", "500"), ("#C0CA33", "600"), ("#AFB42B", "700"), ("#9E9D24", "800"), ("#827717", "900"), ("#F4FF81", "A100"), ("#EEFF41", "A200"), ("#C6FF00", "A400"), ("#AEEA00", "A700")
])
]
fifth_row_palettes = [
("Yellow", [
("#FFF9C4", "100"), ("#FFF59D", "200"), ("#FFF176", "300"), ("#FFEE58", "400"), ("#FFEB3B", "500"), ("#FDD835", "600"), ("#FBC02D", "700"), ("#F9A825", "800"), ("#F57F17", "900"), ("#FFFF8D", "A100"), ("#FFFF00", "A200"), ("#FFEA00", "A400"), ("#FFD600", "A700")
]),
("Amber", [
("#FFECB3", "100"), ("#FFE082", "200"), ("#FFD54F", "300"), ("#FFCA28", "400"), ("#FFC107", "500"), ("#FFB300", "600"), ("#FFA000", "700"), ("#FF8F00", "800"), ("#FF6F00", "900"), ("#FFE57F", "A100"), ("#FFD740", "A200"), ("#FFC400", "A400"), ("#FFAB00", "A700")
]),
("Orange", [
("#FFE0B2", "100"), ("#FFCC80", "200"), ("#FFB74D", "300"), ("#FFA726", "400"), ("#FF9800", "500"), ("#FB8C00", "600"), ("#F57C00", "700"), ("#EF6C00", "800"), ("#E65100", "900"), ("#FFD180", "A100"), ("#FFAB40", "A200"), ("#FF9100", "A400"), ("#FF6D00", "A700")
])
]
for row_palettes in [first_row_palettes, second_row_palettes, third_row_palettes, fourth_row_palettes, fifth_row_palettes]:
row_layout = QHBoxLayout()
row_layout.setSpacing(15)
for palette_name, colors in row_palettes:
column_layout = QVBoxLayout()
column_layout.setSpacing(3)
palette_label = QLabel(palette_name)
palette_label.setStyleSheet("margin-bottom: 2px;")
palette_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
column_layout.addWidget(palette_label)
color_buttons_layout = QHBoxLayout()
color_buttons_layout.setSpacing(3)
for color_hex, color_name in colors:
color_btn = QPushButton()
color_btn.setFixedSize(18, 18)
is_current = color_hex == self.current_theme_color
border_style = "2px solid #fff" if is_current else "none"
color_btn.setStyleSheet(f"""
QPushButton {{
background-color: {color_hex};
border: {border_style};
border-radius: 9px;
}}
QPushButton:hover {{
border: 2px solid #fff;
}}
QPushButton:pressed {{
border: 2px solid #fff;
}}
""")
color_btn.setCursor(Qt.CursorShape.PointingHandCursor)
color_btn.setToolTip(f"{palette_name} {color_name}\n{color_hex}")
color_btn.clicked.connect(lambda checked, color=color_hex, btn=color_btn: self.change_theme_color(color, btn))
self.color_buttons[color_hex] = color_btn
color_buttons_layout.addWidget(color_btn)
column_layout.addLayout(color_buttons_layout)
row_layout.addLayout(column_layout)
grid_layout.addLayout(row_layout)
theme_layout.addLayout(grid_layout)
theme_layout.addStretch()
theme_tab.setLayout(theme_layout)
self.tab_widget.addTab(theme_tab, "Theme")
def change_theme_color(self, color, clicked_btn=None):
if hasattr(self, 'color_buttons'):
for color_hex, btn in self.color_buttons.items():
if color_hex == self.current_theme_color:
btn.setStyleSheet(f"""
QPushButton {{
background-color: {color_hex};
border: none;
border-radius: 9px;
}}
QPushButton:hover {{
border: 2px solid #fff;
}}
QPushButton:pressed {{
border: 2px solid #fff;
}}
""")
break
self.current_theme_color = color
self.settings.setValue('theme_color', color)
self.settings.sync()
if clicked_btn:
clicked_btn.setStyleSheet(f"""
QPushButton {{
background-color: {color};
border: 2px solid #fff;
border-radius: 9px;
}}
QPushButton:hover {{
border: 2px solid #fff;
}}
QPushButton:pressed {{
border: 2px solid #fff;
}}
""")
qdarktheme.setup_theme(
custom_colors={
"[dark]": {
"primary": color,
}
}
)
def setup_about_tab(self): def setup_about_tab(self):
about_tab = QWidget() about_tab = QWidget()
about_layout = QVBoxLayout() about_layout = QVBoxLayout()
about_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) about_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
about_layout.setSpacing(3) about_layout.setSpacing(15)
sections = [ sections = [
("Check for Updates", "https://github.com/afkarxyz/SpotiFLAC/releases"), ("Check for Updates", "Check", "https://github.com/afkarxyz/SpotiFLAC/releases"),
("Report an Issue", "https://github.com/afkarxyz/SpotiFLAC/issues") ("Report an Issue", "Report", "https://github.com/afkarxyz/SpotiFLAC/issues")
] ]
for title, url in sections: for title, button_text, url in sections:
section_widget = QWidget() section_widget = QWidget()
section_layout = QVBoxLayout(section_widget) section_layout = QVBoxLayout(section_widget)
section_layout.setSpacing(10) section_layout.setSpacing(10)
@@ -1048,39 +1229,21 @@ class SpotiFLACGUI(QWidget):
label.setAlignment(Qt.AlignmentFlag.AlignCenter) label.setAlignment(Qt.AlignmentFlag.AlignCenter)
section_layout.addWidget(label) section_layout.addWidget(label)
button = QPushButton("Click Here!") button = QPushButton(button_text)
button.setFixedWidth(150) button.setFixedSize(120, 25)
button.setStyleSheet("""
QPushButton {
background-color: palette(button);
color: palette(button-text);
border: 1px solid palette(mid);
padding: 6px;
border-radius: 15px;
}
QPushButton:hover {
background-color: palette(light);
}
QPushButton:pressed {
background-color: palette(midlight);
}
""")
button.setCursor(Qt.CursorShape.PointingHandCursor) button.setCursor(Qt.CursorShape.PointingHandCursor)
button.clicked.connect(lambda _, url=url: QDesktopServices.openUrl(QUrl(url if url.startswith(('http://', 'https://')) else f'https://{url}'))) button.clicked.connect(lambda _, url=url: QDesktopServices.openUrl(QUrl(url if url.startswith(('http://', 'https://')) else f'https://{url}')))
section_layout.addWidget(button, alignment=Qt.AlignmentFlag.AlignCenter) section_layout.addWidget(button, alignment=Qt.AlignmentFlag.AlignCenter)
about_layout.addWidget(section_widget) about_layout.addWidget(section_widget)
if sections.index((title, url)) < len(sections) - 1:
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
about_layout.addItem(spacer)
footer_label = QLabel("v4.1 | July 2025") footer_label = QLabel(f"v{self.current_version} | July 2025")
footer_label.setStyleSheet("font-size: 12px; margin-top: 10px;") 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)
self.tab_widget.addTab(about_tab, "About") self.tab_widget.addTab(about_tab, "About")
def on_service_changed(self, index): def on_service_changed(self, index):
service = self.service_dropdown.currentData() service = self.service_dropdown.currentData()
self.service = service self.service = service
@@ -1281,8 +1444,7 @@ class SpotiFLACGUI(QWidget):
'artists': playlist_data["playlist_info"]["owner"]["display_name"], 'artists': playlist_data["playlist_info"]["owner"]["display_name"],
'cover': playlist_data["playlist_info"]["owner"]["images"], 'cover': playlist_data["playlist_info"]["owner"]["images"],
'followers': playlist_data["playlist_info"]["followers"]["total"], '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) self.update_display_after_fetch(metadata)
def update_display_after_fetch(self, metadata): def update_display_after_fetch(self, metadata):
@@ -1364,21 +1526,30 @@ class SpotiFLACGUI(QWidget):
def update_button_states(self): def update_button_states(self):
if self.is_single_track: if self.is_single_track:
self.download_selected_btn.hide() for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]:
self.remove_btn.hide() btn.hide()
self.download_all_btn.setText('Download')
self.clear_btn.setText('Clear') self.single_track_container.show()
self.single_download_btn.setEnabled(True)
self.single_clear_btn.setEnabled(True)
else: else:
self.single_track_container.hide()
self.download_selected_btn.show() self.download_selected_btn.show()
self.download_all_btn.show()
self.remove_btn.show() self.remove_btn.show()
self.clear_btn.show()
self.download_all_btn.setText('Download All') self.download_all_btn.setText('Download All')
self.clear_btn.setText('Clear') self.clear_btn.setText('Clear')
self.download_all_btn.show() self.download_all_btn.setMinimumWidth(120)
self.clear_btn.show() self.clear_btn.setMinimumWidth(120)
self.download_selected_btn.setEnabled(True) self.download_selected_btn.setEnabled(True)
self.download_all_btn.setEnabled(True) self.download_all_btn.setEnabled(True)
def hide_track_buttons(self): def hide_track_buttons(self):
buttons = [ buttons = [
@@ -1389,6 +1560,9 @@ class SpotiFLACGUI(QWidget):
] ]
for btn in buttons: for btn in buttons:
btn.hide() btn.hide()
if hasattr(self, 'single_track_container'):
self.single_track_container.hide()
def download_selected(self): def download_selected(self):
if self.is_single_track: if self.is_single_track:
@@ -1426,8 +1600,7 @@ class SpotiFLACGUI(QWidget):
try: try:
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"
@@ -1454,6 +1627,12 @@ class SpotiFLACGUI(QWidget):
def update_ui_for_download_start(self): def update_ui_for_download_start(self):
self.download_selected_btn.setEnabled(False) self.download_selected_btn.setEnabled(False)
self.download_all_btn.setEnabled(False) self.download_all_btn.setEnabled(False)
if hasattr(self, 'single_download_btn'):
self.single_download_btn.setEnabled(False)
if hasattr(self, 'single_clear_btn'):
self.single_clear_btn.setEnabled(False)
self.stop_btn.show() self.stop_btn.show()
self.pause_resume_btn.show() self.pause_resume_btn.show()
self.progress_bar.show() self.progress_bar.show()
@@ -1482,7 +1661,6 @@ class SpotiFLACGUI(QWidget):
self.log_output.append(message) self.log_output.append(message)
else: else:
self.log_output.append(message) self.log_output.append(message)
if percentage > 0 and not "Download progress:" in message: if percentage > 0 and not "Download progress:" in message:
self.progress_bar.setValue(percentage) self.progress_bar.setValue(percentage)
@@ -1502,6 +1680,11 @@ class SpotiFLACGUI(QWidget):
self.download_selected_btn.setEnabled(True) self.download_selected_btn.setEnabled(True)
self.download_all_btn.setEnabled(True) self.download_all_btn.setEnabled(True)
if hasattr(self, 'single_download_btn'):
self.single_download_btn.setEnabled(True)
if hasattr(self, 'single_clear_btn'):
self.single_clear_btn.setEnabled(True)
if success: if success:
self.log_output.append(f"\nStatus: {message}") self.log_output.append(f"\nStatus: {message}")
if failed_tracks: if failed_tracks:
@@ -1558,6 +1741,31 @@ class SpotiFLACGUI(QWidget):
self.timer.stop() self.timer.stop()
self.time_label.hide() self.time_label.hide()
def closeEvent(self, event):
if hasattr(self, 'timer'):
self.timer.stop()
if hasattr(self, 'service_dropdown'):
for attr_name in ['tidal_status_checker', 'deezer_status_checker']:
if hasattr(self.service_dropdown, attr_name):
checker = getattr(self.service_dropdown, attr_name)
if checker.isRunning():
checker.quit()
checker.wait()
if hasattr(self, 'qobuz_region_dropdown'):
for checker in self.qobuz_region_dropdown.status_checkers.values():
if checker.isRunning():
checker.quit()
checker.wait()
if hasattr(self, 'worker') and self.worker and self.worker.isRunning():
self.worker.stop()
self.worker.quit()
self.worker.wait()
event.accept()
if __name__ == '__main__': if __name__ == '__main__':
try: try:
if sys.platform == "win32": if sys.platform == "win32":
@@ -1568,6 +1776,17 @@ if __name__ == '__main__':
pass pass
app = QApplication(sys.argv) app = QApplication(sys.argv)
settings = QSettings('SpotiFLAC', 'Settings')
theme_color = settings.value('theme_color', '#2196F3')
qdarktheme.setup_theme(
custom_colors={
"[dark]": {
"primary": theme_color,
}
}
)
ex = SpotiFLACGUI() ex = SpotiFLACGUI()
ex.show() ex.show()
sys.exit(app.exec()) sys.exit(app.exec())
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+25 -20
View File
@@ -170,27 +170,22 @@ class DeezerDownloader:
print("Downloading FLAC file...") print("Downloading FLAC file...")
try: try:
response = self.session.get(flac_url, stream=True) response = self.session.get(flac_url)
response.raise_for_status() response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
print(f"File size: {total_size} bytes ({total_size / (1024*1024):.2f} MB)")
safe_title = "".join(c for c in metadata.get('title', 'Unknown') if c.isalnum() or c in (' ', '-', '_')).rstrip() safe_title = "".join(c for c in metadata.get('title', 'Unknown') if c.isalnum() or c in (' ', '-', '_')).rstrip()
safe_artist = "".join(c for c in metadata.get('artists', 'Unknown') if c.isalnum() or c in (' ', '-', '_')).rstrip() safe_artist = "".join(c for c in metadata.get('artists', 'Unknown') if c.isalnum() or c in (' ', '-', '_')).rstrip()
filename = f"{safe_artist} - {safe_title}.flac" filename = f"{safe_artist} - {safe_title}.flac"
file_path = os.path.join(output_dir, filename) file_path = os.path.join(output_dir, filename)
downloaded = 0
with open(file_path, 'wb') as f: with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192): f.write(response.content)
f.write(chunk)
downloaded += len(chunk) downloaded = len(response.content)
if self.progress_callback and total_size > 0: print(f"File size: {downloaded} bytes ({downloaded / (1024*1024):.2f} MB)")
current_mb = downloaded / (1024 * 1024)
total_mb = total_size / (1024 * 1024) if self.progress_callback:
percent = (downloaded / total_size) * 100 self.progress_callback(downloaded, downloaded)
self.progress_callback(downloaded, total_size)
print(f"Downloaded: {file_path}") print(f"Downloaded: {file_path}")
@@ -214,19 +209,29 @@ class DeezerDownloader:
return False return False
async def main(): async def main():
if len(sys.argv) != 2: print("=== DeezerDL - Deezer Downloader ===")
print("Usage: python deezerDL.py <ISRC>")
print("Example: python deezerDL.py USUM72409273")
return
isrc = sys.argv[1]
downloader = DeezerDownloader() downloader = DeezerDownloader()
success = await downloader.download_by_isrc(isrc) isrc = "USAT22409172"
output_dir = "."
success = await downloader.download_by_isrc(isrc, output_dir)
if success: if success:
print("Download completed successfully!") print("Download completed successfully!")
else: else:
print("Download failed!") print("Download failed!")
if __name__ == "__main__": 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
asyncio.run(main()) asyncio.run(main())
-130
View File
@@ -1,130 +0,0 @@
import nodriver as uc
import asyncio
async def download_deezer_track(deezer_link=None, initial_delay=7.5):
if deezer_link is None:
deezer_link = "https://www.deezer.com/us/track/2947516331"
browser = None
try:
browser = await uc.start(headless=False)
page = await browser.get("https://deezmate.com/en")
print("Loading...")
await asyncio.sleep(initial_delay)
input_selector = 'input[placeholder="Paste your Deezer link here..."]'
await page.wait_for(input_selector, timeout=15)
input_element = await page.select(input_selector)
await input_element.clear_input()
await input_element.send_keys(deezer_link)
print("Link entered")
await page.evaluate("""
window.apiResponse = null;
window.originalFetch = window.fetch;
window.fetch = function(...args) {
return window.originalFetch(...args).then(async response => {
if (response.url.includes('api.deezmate.com/dl/')) {
try {
const data = await response.clone().json();
window.apiResponse = data;
console.log('Captured API response:', data);
} catch (e) {
console.log('Error parsing API response:', e);
}
}
return response;
});
};
""")
max_retries = 3
download_button_clicked = False
for attempt in range(max_retries):
try:
download_button_selector = 'button.bg-purple.hover\\:bg-purple-dark.cursor-pointer.transition.text-white.rounded-xl.p-2.mt-2.w-full.mb-5'
await page.wait_for(download_button_selector, timeout=15)
download_button = await page.select(download_button_selector)
await download_button.click()
print("Processing...")
download_button_clicked = True
break
except Exception as e:
if attempt < max_retries - 1:
print(f"Turnstile verification failed, retrying... ({attempt + 1}/{max_retries})")
await asyncio.sleep(0.5)
await page.evaluate("window.apiResponse = null;")
else:
print("Failed to pass Turnstile verification after all retries")
raise e
if not download_button_clicked:
return None
try:
track_download_selector = 'button.bg-purple.text-white.flex.items-center.gap-2.px-3.py-1.rounded-full.hover\\:bg-purple-dark.transition'
await page.wait_for(track_download_selector, timeout=15)
track_download_button = await page.select(track_download_selector)
await track_download_button.click()
except Exception as e:
print(f"Failed to click track download button: {e}")
return None
print("Getting FLAC URL from API response...")
api_response = None
for i in range(30):
api_response = await page.evaluate("window.apiResponse")
if api_response:
break
await asyncio.sleep(0.2)
if not api_response:
return None
def parse_nodriver_response(data):
if isinstance(data, list):
result = {}
for item in data:
if isinstance(item, list) and len(item) == 2:
key = item[0]
value_obj = item[1]
if isinstance(value_obj, dict) and 'value' in value_obj:
if value_obj.get('type') == 'object':
result[key] = parse_nodriver_response(value_obj['value'])
else:
result[key] = value_obj['value']
return result
return data
parsed_response = parse_nodriver_response(api_response)
if parsed_response.get('success') and parsed_response.get('links'):
flac_url = parsed_response['links'].get('flac')
if flac_url:
print(f"Successfully obtained FLAC download URL: {flac_url}")
return flac_url
return None
except Exception as e:
print(f"Error: {e}")
return None
finally:
if browser:
try:
await browser.stop()
except:
pass
async def main(deezer_link=None, initial_delay=7.5):
flac_url = await download_deezer_track(deezer_link, initial_delay)
if not flac_url:
print("Failed to download track")
return flac_url
if __name__ == "__main__":
uc.loop().run_until_complete(main())
+28
View File
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-eu" viewBox="0 0 640 480">
<defs>
<g id="eu-d">
<g id="eu-b">
<path id="eu-a" d="m0-1-.3 1 .5.1z"/>
<use xlink:href="#eu-a" transform="scale(-1 1)"/>
</g>
<g id="eu-c">
<use xlink:href="#eu-b" transform="rotate(72)"/>
<use xlink:href="#eu-b" transform="rotate(144)"/>
</g>
<use xlink:href="#eu-c" transform="scale(-1 1)"/>
</g>
</defs>
<path fill="#039" d="M0 0h640v480H0z"/>
<g fill="#fc0" transform="translate(320 242.3)scale(23.7037)">
<use xlink:href="#eu-d" width="100%" height="100%" y="-6"/>
<use xlink:href="#eu-d" width="100%" height="100%" y="6"/>
<g id="eu-e">
<use xlink:href="#eu-d" width="100%" height="100%" x="-6"/>
<use xlink:href="#eu-d" width="100%" height="100%" transform="rotate(-144 -2.3 -2.1)"/>
<use xlink:href="#eu-d" width="100%" height="100%" transform="rotate(144 -2.1 -2.3)"/>
<use xlink:href="#eu-d" width="100%" height="100%" transform="rotate(72 -4.7 -2)"/>
<use xlink:href="#eu-d" width="100%" height="100%" transform="rotate(72 -5 .5)"/>
</g>
<use xlink:href="#eu-e" width="100%" height="100%" transform="scale(-1 1)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FF0000;stroke:#373435;stroke-width:0.5669;stroke-miterlimit:10;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFF00;stroke:#373435;stroke-width:0.5669;stroke-miterlimit:10;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#2AA125;stroke:#373435;stroke-width:0.5669;stroke-miterlimit:10;}
</style>
<g id="SVGRepo_bgCarrier">
</g>
<g id="SVGRepo_tracerCarrier">
</g>
<g id="Layer_x0020_1">
<g id="_1818452274576">
<g id="SVGRepo_bgCarrier_00000044893734704698182460000014884511992085247122_">
</g>
<g id="SVGRepo_tracerCarrier_00000067939915892718314930000001743019108017086612_">
</g>
<g id="SVGRepo_iconCarrier_00000176000695080737548300000008661679408724292005_">
<path d="M407.1,227.2c-54.7-27.6-119.3-43.8-187.6-43.8c-38.9,0-76.5,5.2-112.3,15l3-0.7c-2.1,0.7-4.5,1.1-7,1.1
c-13,0-23.5-10.5-23.5-23.5c0-10.5,6.8-19.3,16.3-22.4l0.2-0.1c90.9-26.9,240.6-21.8,335.4,34.6c7.3,4.4,12.1,12.3,12.1,21.3
c0,4.4-1.2,8.6-3.2,12.2l0.1-0.1c-5,5.9-12.3,9.6-20.6,9.6c-4.7,0-9-1.2-12.8-3.3L407.1,227.2L407.1,227.2z M404.5,298.9
c-3.4,5.7-9.6,9.4-16.6,9.4c-3.8,0-7.4-1.1-10.4-3l0.1,0.1c-46.8-26.8-102.8-42.5-162.5-42.5c-32.9,0-64.6,4.8-94.6,13.7l2.3-0.6
c-1.7,0.5-3.7,0.9-5.8,0.9c-10.7,0-19.4-8.7-19.4-19.4c0-8.7,5.7-16,13.5-18.5l0.1,0c30.8-9.1,66.2-14.4,102.8-14.4
c68.1,0,132,18.2,187,49.9l-1.8-1c5.1,3.3,8.4,8.9,8.4,15.3C407.7,292.5,406.5,296,404.5,298.9L404.5,298.9L404.5,298.9
L404.5,298.9z M373.8,369.3c-2.7,4.6-7.6,7.7-13.3,7.7c-3.2,0-6.1-1-8.6-2.6l0.1,0c-40.9-23-89.7-36.5-141.7-36.5
c-29.8,0-58.6,4.5-85.7,12.7l2.1-0.5c-1.1,0.3-2.5,0.5-3.8,0.5c-8.7,0-15.8-7.1-15.8-15.8c0-7.4,5.1-13.6,11.9-15.3l0.1,0
c27-8,58-12.6,90.1-12.6c58.1,0,112.6,15.1,159.9,41.7l-1.7-0.9c5.2,2.6,8.6,7.8,8.6,13.8C376,364.3,375.2,367.1,373.8,369.3
L373.8,369.3L373.8,369.3L373.8,369.3z M256-0.6L256-0.6C114.6-0.6,0,114,0,255.4s114.6,256,256,256s256-114.6,256-256l0,0
C511.6,114.2,397.2-0.2,256-0.6L256-0.6L256-0.6L256-0.6z"/>
</g>
</g>
<path class="st0" d="M406.9,227.2c0,0,0.1,0,0.1,0.1L406.9,227.2z M107.1,198.5c35.8-9.8,73.4-15,112.3-15
c68.3,0,132.8,16.1,187.5,43.7c3.8,2.1,8.2,3.3,12.8,3.3c8.2,0,15.6-3.7,20.5-9.6c0,0,0,0,0,0c2-3.6,3.1-7.7,3.1-12
c0-9-4.8-16.8-12.1-21.3c-94.7-56.4-244.5-61.5-335.4-34.6l-0.2,0.1c-9.4,3.1-16.3,11.9-16.3,22.4c0,13,10.5,23.5,23.5,23.5
c2.5,0,4.9-0.4,7-1.1L107.1,198.5L107.1,198.5z"/>
<path class="st1" d="M401.2,274.3c-55-31.8-118.9-49.9-187-49.9c-36.6,0-72,5.3-102.8,14.4l-0.1,0c-7.9,2.5-13.5,9.9-13.5,18.5
c0,10.7,8.7,19.4,19.4,19.4c1.3,0,2.5-0.1,3.6-0.3c29.9-8.8,61.5-13.6,94.3-13.6c59.7,0,115.7,15.7,162.4,42.5l0.1,0.1
c3,1.9,6.5,3,10.3,3c7,0,13.1-3.7,16.6-9.4l0,0c2-2.9,3.2-6.5,3.2-10.3c0-6.4-3.3-12-8.4-15.3L401.2,274.3L401.2,274.3z"/>
<path class="st2" d="M352,374.4C352,374.4,352,374.4,352,374.4L352,374.4z M373.8,369.3C373.8,369.3,373.7,369.4,373.8,369.3
L373.8,369.3L373.8,369.3z M367.8,347.8l-0.4-0.2C367.5,347.6,367.6,347.7,367.8,347.8z M369,348.4
c-47.3-26.5-101.8-41.7-159.9-41.7c-32.1,0-63.1,4.6-90.1,12.6l-0.1,0c-6.8,1.8-11.9,8-11.9,15.3c0,8.7,7.1,15.8,15.8,15.8
c1,0,1.9-0.1,2.8-0.3c26.8-8.1,55.2-12.4,84.6-12.4c52,0,100.8,13.5,141.7,36.5l0,0c2.4,1.6,5.4,2.6,8.5,2.6
c5.7,0,10.6-3.1,13.3-7.7h0c1.4-2.3,2.2-5,2.2-7.9c0-5.9-3.3-11-8.2-13.6L369,348.4L369,348.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+18 -38
View File
@@ -124,45 +124,25 @@ class QobuzDownloader:
print(f"Downloading...") print(f"Downloading...")
try: try:
with self.session.get(download_url, stream=True, timeout=900) as response, \ response = self.session.get(download_url, timeout=900)
open(temp_filename, 'wb') as f: response.raise_for_status()
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0)) if is_stopped_callback and is_stopped_callback():
downloaded_size = 0 raise Exception("Download stopped")
start_time = time.time()
last_update_time = start_time
for chunk in response.iter_content(chunk_size=self.download_chunk_size): while is_paused_callback and is_paused_callback():
if is_stopped_callback and is_stopped_callback(): time.sleep(0.1)
f.close() if is_stopped_callback and is_stopped_callback():
if os.path.exists(temp_filename): raise Exception("Download stopped")
os.remove(temp_filename)
raise Exception("Download stopped") with open(temp_filename, 'wb') as f:
f.write(response.content)
while is_paused_callback and is_paused_callback():
time.sleep(0.1) downloaded_size = len(response.content)
if is_stopped_callback and is_stopped_callback(): total_size = downloaded_size
f.close()
if os.path.exists(temp_filename): if self.progress_callback:
os.remove(temp_filename) self.progress_callback(downloaded_size, total_size)
raise Exception("Download stopped")
f.write(chunk)
downloaded_size += len(chunk)
current_time = time.time()
if current_time - last_update_time >= 1:
if total_size > 0:
progress_percent = (downloaded_size / total_size) * 100
elapsed_time = current_time - start_time
speed = downloaded_size / (1024 * 1024 * elapsed_time) if elapsed_time > 0 else 0
print(f"{progress_percent:.2f}% - {speed:.2f} MB/s")
else:
print(f"{downloaded_size / (1024 * 1024):.2f} MB")
last_update_time = current_time
if self.progress_callback:
self.progress_callback(downloaded_size, total_size)
os.rename(temp_filename, output_filename) os.rename(temp_filename, output_filename)
print("Download complete") print("Download complete")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+109 -136
View File
@@ -3,7 +3,7 @@ import json
import os import os
import re import re
import time import time
import httpx import requests
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType from mutagen.id3 import PictureType
@@ -35,7 +35,7 @@ class TidalDownloader:
sanitized = re.sub(r'[\\/*?:"<>|]', "", str(filename)) sanitized = re.sub(r'[\\/*?:"<>|]', "", str(filename))
return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track" return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track"
async def get_access_token(self): def get_access_token(self):
refresh_url = "https://auth.tidal.com/v1/oauth2/token" refresh_url = "https://auth.tidal.com/v1/oauth2/token"
payload = { payload = {
@@ -43,68 +43,67 @@ class TidalDownloader:
"grant_type": "client_credentials", "grant_type": "client_credentials",
} }
async with httpx.AsyncClient(http2=True) as client:
try:
response = await client.post(
url=refresh_url,
data=payload,
auth=(self.client_id, self.client_secret),
)
if response.status_code == 200:
token_data = response.json()
return token_data.get("access_token")
else:
return None
except:
return None
async def search_tracks(self, query):
try: try:
tidal_token = await self.get_access_token() response = requests.post(
url=refresh_url,
data=payload,
auth=(self.client_id, self.client_secret),
timeout=self.timeout
)
if response.status_code == 200:
token_data = response.json()
return token_data.get("access_token")
else:
return None
except:
return None
def search_tracks(self, query):
try:
tidal_token = self.get_access_token()
if not tidal_token: if not tidal_token:
raise Exception("Failed to get access token") raise Exception("Failed to get access token")
search_url = f"https://api.tidal.com/v1/search/tracks?query={query}&limit=25&offset=0&countryCode=US" search_url = f"https://api.tidal.com/v1/search/tracks?query={query}&limit=25&offset=0&countryCode=US"
header = {"authorization": f"Bearer {tidal_token}"} header = {"authorization": f"Bearer {tidal_token}"}
async with httpx.AsyncClient(http2=True) as client: search_data = requests.get(url=search_url, headers=header, timeout=self.timeout)
search_data = await client.get(url=search_url, headers=header) response_data = search_data.json()
response_data = search_data.json()
filtered_items = [{
filtered_items = [{ "id": item.get("id"),
"id": item.get("id"), "title": item.get("title"),
"title": item.get("title"), "url": item.get("url"),
"url": item.get("url"), "isrc": item.get("isrc"),
"isrc": item.get("isrc"), "audioQuality": item.get("audioQuality"),
"audioQuality": item.get("audioQuality"), "mediaMetadata": item.get("mediaMetadata"),
"mediaMetadata": item.get("mediaMetadata"), "album": item.get("album", {}),
"album": item.get("album", {}), "artists": item.get("artists", []),
"artists": item.get("artists", []), "artist": item.get("artist", {}),
"artist": item.get("artist", {}), "trackNumber": item.get("trackNumber"),
"trackNumber": item.get("trackNumber"), "volumeNumber": item.get("volumeNumber"),
"volumeNumber": item.get("volumeNumber"), "duration": item.get("duration"),
"duration": item.get("duration"), "copyright": item.get("copyright"),
"copyright": item.get("copyright"), "explicit": item.get("explicit")
"explicit": item.get("explicit") } for item in response_data.get("items", [])]
} for item in response_data.get("items", [])]
return {
return { "limit": response_data.get("limit"),
"limit": response_data.get("limit"), "offset": response_data.get("offset"),
"offset": response_data.get("offset"), "totalNumberOfItems": response_data.get("totalNumberOfItems"),
"totalNumberOfItems": response_data.get("totalNumberOfItems"), "items": filtered_items
"items": filtered_items }
}
except Exception as e: except Exception as e:
raise Exception(f"Search error: {str(e)}") raise Exception(f"Search error: {str(e)}")
async def get_track_info(self, query, isrc=None): def get_track_info(self, query, isrc=None):
print(f"Fetching: {query}" + (f" (ISRC: {isrc})" if isrc else "")) print(f"Fetching: {query}" + (f" (ISRC: {isrc})" if isrc else ""))
try: try:
result = await self.search_tracks(query) result = self.search_tracks(query)
if not result or not result.get("items"): if not result or not result.get("items"):
raise Exception(f"No tracks found for query: {query}") raise Exception(f"No tracks found for query: {query}")
@@ -143,99 +142,73 @@ class TidalDownloader:
except Exception as e: except Exception as e:
raise Exception(f"Error getting track info: {str(e)}") raise Exception(f"Error getting track info: {str(e)}")
async def get_download_url(self, track_id, quality="LOSSLESS"): def get_download_url(self, track_id, quality="LOSSLESS"):
print("Fetching URL...") print("Fetching URL...")
download_api_url = f"https://hifi.401658.xyz/track/?id={track_id}&quality={quality}" download_api_url = f"https://hifi.401658.xyz/track/?id={track_id}&quality={quality}"
async with httpx.AsyncClient(http2=True, timeout=self.timeout) as client: try:
try: response = requests.get(download_api_url, timeout=self.timeout)
response = await client.get(download_api_url)
if response.status_code == 200:
data = response.json()
if response.status_code == 200: for item in data:
data = response.json() if "OriginalTrackUrl" in item:
print("URL found")
for item in data: return {
if "OriginalTrackUrl" in item: "download_url": item["OriginalTrackUrl"],
print("URL found") "track_info": data[0] if data else {}
return { }
"download_url": item["OriginalTrackUrl"],
"track_info": data[0] if data else {} raise Exception("Download URL not found in response")
} else:
raise Exception(f"API returned status code: {response.status_code}")
raise Exception("Download URL not found in response")
else: except Exception as e:
raise Exception(f"API returned status code: {response.status_code}") raise Exception(f"Error getting download URL: {str(e)}")
except Exception as e:
raise Exception(f"Error getting download URL: {str(e)}")
async def download_album_art(self, album_id, size="1280x1280"): def download_album_art(self, album_id, size="1280x1280"):
try: try:
art_url = f"https://resources.tidal.com/images/{album_id.replace('-', '/')}/{size}.jpg" art_url = f"https://resources.tidal.com/images/{album_id.replace('-', '/')}/{size}.jpg"
async with httpx.AsyncClient(http2=True, timeout=self.timeout) as client: response = requests.get(art_url, timeout=self.timeout)
response = await client.get(art_url)
if response.status_code == 200:
return response.content
else:
print(f"Failed to download album art: HTTP {response.status_code}")
return None
if response.status_code == 200:
return response.content
else:
print(f"Failed to download album art: HTTP {response.status_code}")
return None
except Exception as e: except Exception as e:
print(f"Error downloading album art: {str(e)}") print(f"Error downloading album art: {str(e)}")
return None return None
async def download_file(self, url, filepath, is_paused_callback=None, is_stopped_callback=None): def download_file(self, url, filepath, is_paused_callback=None, is_stopped_callback=None):
temp_filepath = filepath + ".part" temp_filepath = filepath + ".part"
retry_count = 0 retry_count = 0
while retry_count <= self.max_retries: while retry_count <= self.max_retries:
try: try:
async with httpx.AsyncClient(http2=True, timeout=60.0) as client: response = requests.get(url, timeout=60.0)
async with client.stream('GET', url) as response: if response.status_code != 200:
if response.status_code != 200: raise Exception(f"HTTP {response.status_code}")
raise Exception(f"HTTP {response.status_code}")
if is_stopped_callback and is_stopped_callback():
total_size = int(response.headers.get('content-length', 0)) raise Exception("Download stopped")
downloaded_size = 0
start_time = time.time() while is_paused_callback and is_paused_callback():
last_update_time = start_time time.sleep(0.1)
if is_stopped_callback and is_stopped_callback():
with open(temp_filepath, 'wb') as f: raise Exception("Download stopped")
async for chunk in response.aiter_bytes(chunk_size=self.download_chunk_size):
if is_stopped_callback and is_stopped_callback(): with open(temp_filepath, 'wb') as f:
f.close() f.write(response.content)
if os.path.exists(temp_filepath):
os.remove(temp_filepath) downloaded_size = len(response.content)
raise Exception("Download stopped")
if self.progress_callback:
while is_paused_callback and is_paused_callback(): self.progress_callback(downloaded_size, downloaded_size)
await asyncio.sleep(0.1)
if is_stopped_callback and is_stopped_callback():
f.close()
if os.path.exists(temp_filepath):
os.remove(temp_filepath)
raise Exception("Download stopped")
f.write(chunk)
downloaded_size += len(chunk)
current_time = time.time()
if current_time - last_update_time >= 1:
if total_size > 0:
progress_percent = (downloaded_size / total_size) * 100
elapsed_time = current_time - start_time
speed = downloaded_size / (1024 * 1024 * elapsed_time) if elapsed_time > 0 else 0
print(f"{progress_percent:.2f}% - {speed:.2f} MB/s")
else:
print(f"{downloaded_size / (1024 * 1024):.2f} MB")
last_update_time = current_time
if self.progress_callback:
self.progress_callback(downloaded_size, total_size)
os.rename(temp_filepath, filepath) os.rename(temp_filepath, filepath)
print("Download complete") print("Download complete")
return {"success": True, "size": downloaded_size} return {"success": True, "size": downloaded_size}
@@ -252,9 +225,9 @@ class TidalDownloader:
print(f"Download error (attempt {retry_count}/{self.max_retries}): {str(e)}") print(f"Download error (attempt {retry_count}/{self.max_retries}): {str(e)}")
print(f"Retrying in {retry_count * 2} seconds...") print(f"Retrying in {retry_count * 2} seconds...")
await asyncio.sleep(retry_count * 2) time.sleep(retry_count * 2)
async def embed_metadata(self, filepath, track_info, search_info=None): def embed_metadata(self, filepath, track_info, search_info=None):
try: try:
print("Embedding metadata...") print("Embedding metadata...")
audio = FLAC(filepath) audio = FLAC(filepath)
@@ -325,7 +298,7 @@ class TidalDownloader:
audio["COMMENT"] = f"Tidal {track_info['audioQuality']}" audio["COMMENT"] = f"Tidal {track_info['audioQuality']}"
if album_info.get("cover"): if album_info.get("cover"):
album_art = await self.download_album_art(album_info["cover"]) album_art = self.download_album_art(album_info["cover"])
if album_art: if album_art:
picture = Picture() picture = Picture()
picture.data = album_art picture.data = album_art
@@ -343,14 +316,14 @@ class TidalDownloader:
print(f"Error embedding metadata: {str(e)}") print(f"Error embedding metadata: {str(e)}")
return False return False
async def download(self, query, isrc=None, output_dir=".", quality="LOSSLESS", is_paused_callback=None, is_stopped_callback=None): def download(self, query, isrc=None, output_dir=".", quality="LOSSLESS", is_paused_callback=None, is_stopped_callback=None):
if output_dir != ".": if output_dir != ".":
try: try:
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
except OSError as e: except OSError as e:
raise Exception(f"Directory error: {e}") raise Exception(f"Directory error: {e}")
track_info = await self.get_track_info(query, isrc) track_info = self.get_track_info(query, isrc)
track_id = track_info.get("id") track_id = track_info.get("id")
if not track_id: if not track_id:
@@ -376,12 +349,12 @@ class TidalDownloader:
print(f"File already exists: {output_filename} ({file_size / (1024 * 1024):.2f} MB)") print(f"File already exists: {output_filename} ({file_size / (1024 * 1024):.2f} MB)")
return output_filename return output_filename
download_info = await self.get_download_url(track_id, quality) download_info = self.get_download_url(track_id, quality)
download_url = download_info["download_url"] download_url = download_info["download_url"]
download_track_info = download_info["track_info"] download_track_info = download_info["track_info"]
print(f"Downloading to: {output_filename}") print(f"Downloading to: {output_filename}")
await self.download_file( self.download_file(
download_url, download_url,
output_filename, output_filename,
is_paused_callback=is_paused_callback, is_paused_callback=is_paused_callback,
@@ -390,7 +363,7 @@ class TidalDownloader:
print("Adding metadata...") print("Adding metadata...")
try: try:
await self.embed_metadata(output_filename, download_track_info, track_info) self.embed_metadata(output_filename, download_track_info, track_info)
print("Metadata saved") print("Metadata saved")
except Exception as e: except Exception as e:
print(f"Tagging failed: {e}") print(f"Tagging failed: {e}")
@@ -398,7 +371,7 @@ class TidalDownloader:
print("Done") print("Done")
return output_filename return output_filename
async def main(): def main():
print("=== TidalDL - Tidal Downloader ===") print("=== TidalDL - Tidal Downloader ===")
downloader = TidalDownloader(timeout=30, max_retries=3) downloader = TidalDownloader(timeout=30, max_retries=3)
@@ -407,7 +380,7 @@ async def main():
output_dir = "." output_dir = "."
try: try:
downloaded_file = await downloader.download(query, isrc, output_dir) downloaded_file = downloader.download(query, isrc, output_dir)
print(f"Success: File saved as {downloaded_file}") print(f"Success: File saved as {downloaded_file}")
except Exception as e: except Exception as e:
print(f"Error: {str(e)}") print(f"Error: {str(e)}")
@@ -425,4 +398,4 @@ if __name__ == "__main__":
except: except:
pass pass
asyncio.run(main()) main()
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
<path fill="#bd3d44" d="M0 0h640v480H0"/>
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
<path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
<marker id="us-a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
</marker>
<path fill="none" marker-mid="url(#us-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "4.0" "version": "4.1"
} }