diff --git a/SpotiFLAC.py b/SpotiFLAC.py index a2a291f..9ffd3e6 100644 --- a/SpotiFLAC.py +++ b/SpotiFLAC.py @@ -2,15 +2,17 @@ import sys import os from dataclasses import dataclass from datetime import datetime +from pathlib import Path import requests import re import asyncio from packaging import version +import qdarktheme from PyQt6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QFileDialog, QListWidget, QTextEdit, QTabWidget, QButtonGroup, QRadioButton, - QAbstractItemView, QSpacerItem, QSizePolicy, QProgressBar, QCheckBox, QDialog, + QAbstractItemView, QProgressBar, QCheckBox, QDialog, QDialogButtonBox, QComboBox, QStyledItemDelegate ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer, QTime, QSettings, QSize @@ -82,7 +84,7 @@ class DownloadWorker(QThread): filename = f"{track.title}.flac" else: 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): try: @@ -94,16 +96,12 @@ class DownloadWorker(QThread): downloader = DeezerDownloader() else: downloader = TidalDownloader() - def progress_update(current, total): if total > 0: percent = (current / total) * 100 - current_mb = current / (1024 * 1024) - total_mb = total / (1024 * 1024) - self.progress.emit(f"Download progress: {percent:.2f}% ({current_mb:.2f}MB/{total_mb:.2f}MB)", - int(percent)) + self.progress.emit("", int(percent)) else: - self.progress.emit(f"Processing metadata...", 0) + self.progress.emit("Processing metadata...", 0) downloader.set_progress_callback(progress_update) @@ -133,7 +131,7 @@ class DownloadWorker(QThread): else: 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) 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_stopped_callback = lambda: self.is_stopped - try: - 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( + download_result_details = downloader.download( query=f"{track.title} {track.artists}", isrc=track.isrc, output_dir=track_outpath, quality="LOSSLESS", is_paused_callback=is_paused_callback, is_stopped_callback=is_stopped_callback - )) + ) if isinstance(download_result_details, str) and os.path.exists(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) - try: - 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)) + success = asyncio.run(downloader.download_by_isrc(track.isrc, track_outpath)) if success: 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): def __init__(self, current_version, new_version, parent=None): super().__init__(parent) - self.setWindowTitle("Update Available") + self.setWindowTitle("Update Now") self.setFixedWidth(400) self.setModal(True) layout = QVBoxLayout() - message = QLabel(f"A new version of SpotiFLAC is available!\n\n" - f"Current version: v{current_version}\n" - f"New version: v{new_version}") + message = QLabel(f"SpotiFLAC v{new_version} Available!") message.setWordWrap(True) 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() - self.update_button = QPushButton("Update") + self.update_button = QPushButton("Check") self.update_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.cancel_button = QPushButton("Cancel") + self.cancel_button = QPushButton("Later") self.cancel_button.setCursor(Qt.CursorShape.PointingHandCursor) button_box.addButton(self.update_button, QDialogButtonBox.ButtonRole.AcceptRole) @@ -342,8 +316,6 @@ class UpdateDialog(QDialog): self.update_button.clicked.connect(self.accept) self.cancel_button.clicked.connect(self.reject) - - class TidalStatusChecker(QThread): status_updated = pyqtSignal(bool) @@ -398,7 +370,7 @@ class StatusIndicatorDelegate(QStyledItemDelegate): circle_size = 6 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.setPen(Qt.PenStyle.NoPen) @@ -422,7 +394,7 @@ class ServiceComboBox(QComboBox): self.tidal_status_timer = QTimer(self) 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.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.timeout.connect(self.refresh_deezer_status) - self.deezer_status_timer.start(6000) + self.deezer_status_timer.start(60000) def setup_items(self): current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -458,10 +430,10 @@ class ServiceComboBox(QComboBox): pixmap.fill(Qt.GlobalColor.transparent) 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()): - service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) - if service_id == 'tidal': + current_service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) + if current_service_id == service_id: service_data = self.itemData(i, Qt.ItemDataRole.UserRole) if isinstance(service_data, dict): service_data['online'] = is_online @@ -469,24 +441,27 @@ class ServiceComboBox(QComboBox): break self.update() + def update_tidal_service_status(self, is_online): + self.update_service_status('tidal', is_online) + 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.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.start() def update_deezer_service_status(self, is_online): - for i in range(self.count()): - 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() + self.update_service_status('deezer', is_online) 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.status_updated.connect(self.update_deezer_service_status) 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.timeout.connect(self.check_status) - self.status_timer.start(10000) + self.status_timer.start(60000) def setup_items(self): current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -566,6 +541,12 @@ class QobuzRegionComboBox(QComboBox): self.update() 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: region_id = region['id'] checker = QobuzStatusChecker(region_id) @@ -583,13 +564,13 @@ class QobuzRegionComboBox(QComboBox): class SpotiFLACGUI(QWidget): def __init__(self): super().__init__() - self.current_version = "4.1" + self.current_version = "4.2" self.tracks = [] self.all_tracks = [] self.reset_state() 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.filename_format = self.settings.value('filename_format', 'title_artist') @@ -598,6 +579,7 @@ class SpotiFLACGUI(QWidget): self.service = self.settings.value('service', 'tidal') self.qobuz_region = self.settings.value('qobuz_region', 'us') 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.timer = QTimer(self) @@ -611,6 +593,13 @@ class SpotiFLACGUI(QWidget): if self.check_for_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): try: 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) 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: QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases")) @@ -682,12 +668,13 @@ class SpotiFLACGUI(QWidget): spotify_label.setFixedWidth(100) 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.setText(self.last_url) self.spotify_url.textChanged.connect(self.save_url) self.fetch_btn = QPushButton('Fetch') + self.fetch_btn.setFixedWidth(80) self.fetch_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.fetch_btn.clicked.connect(self.fetch_tracks) @@ -730,6 +717,7 @@ class SpotiFLACGUI(QWidget): self.setup_dashboard_tab() self.setup_process_tab() self.setup_settings_tab() + self.setup_theme_tab() self.setup_about_tab() def setup_dashboard_tab(self): @@ -745,6 +733,7 @@ class SpotiFLACGUI(QWidget): self.setup_track_buttons() dashboard_layout.addLayout(self.btn_layout) + dashboard_layout.addWidget(self.single_track_container) dashboard_tab.setLayout(dashboard_layout) self.tab_widget.addTab(dashboard_tab, "Dashboard") @@ -814,7 +803,7 @@ class SpotiFLACGUI(QWidget): search_layout.addLayout(search_input_layout) self.search_widget.setLayout(search_layout) - self.search_widget.hide() + self.search_widget.hide() def setup_track_buttons(self): self.btn_layout = QHBoxLayout() @@ -824,7 +813,7 @@ class SpotiFLACGUI(QWidget): self.clear_btn = QPushButton('Clear') 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) self.download_selected_btn.clicked.connect(self.download_selected) @@ -834,8 +823,29 @@ class SpotiFLACGUI(QWidget): self.btn_layout.addStretch() 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.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): self.process_tab = QWidget() @@ -862,13 +872,19 @@ class SpotiFLACGUI(QWidget): self.stop_btn = QPushButton('Stop') 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.pause_resume_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.stop_btn.clicked.connect(self.stop_download) self.pause_resume_btn.clicked.connect(self.toggle_pause_resume) + + control_layout.addStretch() control_layout.addWidget(self.stop_btn) control_layout.addWidget(self.pause_resume_btn) + control_layout.addStretch() process_layout.addLayout(control_layout) @@ -901,6 +917,7 @@ class SpotiFLACGUI(QWidget): self.output_dir.textChanged.connect(self.save_settings) self.output_browse = QPushButton('Browse') + self.output_browse.setFixedWidth(80) self.output_browse.setCursor(Qt.CursorShape.PointingHandCursor) self.output_browse.clicked.connect(self.browse_output) @@ -1008,15 +1025,8 @@ class SpotiFLACGUI(QWidget): settings_layout.addStretch() settings_tab.setLayout(settings_layout) self.tab_widget.addTab(settings_tab, "Settings") - for i in range(self.service_dropdown.count()): - if self.service_dropdown.itemData(i, Qt.ItemDataRole.UserRole + 1) == self.service: - 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 + self.set_combobox_value(self.service_dropdown, self.service) + self.set_combobox_value(self.qobuz_region_dropdown, self.qobuz_region) @@ -1026,18 +1036,189 @@ class SpotiFLACGUI(QWidget): 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): about_tab = QWidget() about_layout = QVBoxLayout() about_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - about_layout.setSpacing(3) + about_layout.setSpacing(15) sections = [ - ("Check for Updates", "https://github.com/afkarxyz/SpotiFLAC/releases"), - ("Report an Issue", "https://github.com/afkarxyz/SpotiFLAC/issues") + ("Check for Updates", "Check", "https://github.com/afkarxyz/SpotiFLAC/releases"), + ("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_layout = QVBoxLayout(section_widget) section_layout.setSpacing(10) @@ -1048,39 +1229,21 @@ class SpotiFLACGUI(QWidget): label.setAlignment(Qt.AlignmentFlag.AlignCenter) section_layout.addWidget(label) - button = QPushButton("Click Here!") - button.setFixedWidth(150) - 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 = QPushButton(button_text) + button.setFixedSize(120, 25) button.setCursor(Qt.CursorShape.PointingHandCursor) 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) 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.setStyleSheet("font-size: 12px; margin-top: 10px;") + footer_label = QLabel(f"v{self.current_version} | July 2025") + footer_label.setStyleSheet("font-size: 12px; margin-top: 20px;") about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter) 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): service = self.service_dropdown.currentData() self.service = service @@ -1281,8 +1444,7 @@ 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 update_display_after_fetch(self, metadata): @@ -1364,21 +1526,30 @@ class SpotiFLACGUI(QWidget): def update_button_states(self): if self.is_single_track: - self.download_selected_btn.hide() - self.remove_btn.hide() - self.download_all_btn.setText('Download') - self.clear_btn.setText('Clear') + for btn in [self.download_selected_btn, self.download_all_btn, self.remove_btn, self.clear_btn]: + btn.hide() + + self.single_track_container.show() + + self.single_download_btn.setEnabled(True) + self.single_clear_btn.setEnabled(True) + else: + self.single_track_container.hide() + self.download_selected_btn.show() + self.download_all_btn.show() self.remove_btn.show() + self.clear_btn.show() + self.download_all_btn.setText('Download All') self.clear_btn.setText('Clear') - - self.download_all_btn.show() - self.clear_btn.show() - - self.download_selected_btn.setEnabled(True) - self.download_all_btn.setEnabled(True) + + self.download_all_btn.setMinimumWidth(120) + self.clear_btn.setMinimumWidth(120) + + self.download_selected_btn.setEnabled(True) + self.download_all_btn.setEnabled(True) def hide_track_buttons(self): buttons = [ @@ -1389,6 +1560,9 @@ class SpotiFLACGUI(QWidget): ] for btn in buttons: btn.hide() + + if hasattr(self, 'single_track_container'): + self.single_track_container.hide() def download_selected(self): if self.is_single_track: @@ -1426,8 +1600,7 @@ class SpotiFLACGUI(QWidget): try: self.start_download_worker(tracks_to_download, outpath) except Exception as e: - self.log_output.append(f"Error: An error occurred while starting the download: {str(e)}") - + self.log_output.append(f"Error: An error occurred while starting the download: {str(e)}") def start_download_worker(self, tracks_to_download, outpath): service = self.service_dropdown.currentData() qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us" @@ -1454,6 +1627,12 @@ class SpotiFLACGUI(QWidget): def update_ui_for_download_start(self): self.download_selected_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.pause_resume_btn.show() self.progress_bar.show() @@ -1482,7 +1661,6 @@ class SpotiFLACGUI(QWidget): self.log_output.append(message) else: self.log_output.append(message) - if percentage > 0 and not "Download progress:" in message: self.progress_bar.setValue(percentage) @@ -1502,6 +1680,11 @@ class SpotiFLACGUI(QWidget): self.download_selected_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: self.log_output.append(f"\nStatus: {message}") if failed_tracks: @@ -1558,6 +1741,31 @@ class SpotiFLACGUI(QWidget): self.timer.stop() 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__': try: if sys.platform == "win32": @@ -1568,6 +1776,17 @@ if __name__ == '__main__': pass 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.show() sys.exit(app.exec()) \ No newline at end of file diff --git a/deezer.png b/deezer.png new file mode 100644 index 0000000..db9cdd1 Binary files /dev/null and b/deezer.png differ diff --git a/deezerDL.py b/deezerDL.py index 305016d..4d0d22b 100644 --- a/deezerDL.py +++ b/deezerDL.py @@ -170,27 +170,22 @@ class DeezerDownloader: print("Downloading FLAC file...") try: - response = self.session.get(flac_url, stream=True) + response = self.session.get(flac_url) 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_artist = "".join(c for c in metadata.get('artists', 'Unknown') if c.isalnum() or c in (' ', '-', '_')).rstrip() filename = f"{safe_artist} - {safe_title}.flac" file_path = os.path.join(output_dir, filename) - downloaded = 0 with open(file_path, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - downloaded += len(chunk) - if self.progress_callback and total_size > 0: - current_mb = downloaded / (1024 * 1024) - total_mb = total_size / (1024 * 1024) - percent = (downloaded / total_size) * 100 - self.progress_callback(downloaded, total_size) + f.write(response.content) + + downloaded = len(response.content) + print(f"File size: {downloaded} bytes ({downloaded / (1024*1024):.2f} MB)") + + if self.progress_callback: + self.progress_callback(downloaded, downloaded) print(f"Downloaded: {file_path}") @@ -214,19 +209,29 @@ class DeezerDownloader: return False async def main(): - if len(sys.argv) != 2: - print("Usage: python deezerDL.py ") - print("Example: python deezerDL.py USUM72409273") - return - - isrc = sys.argv[1] + print("=== DeezerDL - Deezer Downloader ===") downloader = DeezerDownloader() - success = await downloader.download_by_isrc(isrc) + isrc = "USAT22409172" + output_dir = "." + + success = await downloader.download_by_isrc(isrc, output_dir) if success: print("Download completed successfully!") else: print("Download failed!") 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()) \ No newline at end of file diff --git a/eu.svg b/eu.svg new file mode 100644 index 0000000..b0874c1 --- /dev/null +++ b/eu.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..8686c16 Binary files /dev/null and b/icon.ico differ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..455ce78 --- /dev/null +++ b/icon.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qobuz.png b/qobuz.png new file mode 100644 index 0000000..a66fe4b Binary files /dev/null and b/qobuz.png differ diff --git a/qobuzDL.py b/qobuzDL.py index b32bd89..ada4e74 100644 --- a/qobuzDL.py +++ b/qobuzDL.py @@ -124,45 +124,25 @@ class QobuzDownloader: print(f"Downloading...") try: - with self.session.get(download_url, stream=True, timeout=900) as response, \ - open(temp_filename, 'wb') as f: - response.raise_for_status() - total_size = int(response.headers.get('content-length', 0)) - downloaded_size = 0 - start_time = time.time() - last_update_time = start_time + response = self.session.get(download_url, timeout=900) + response.raise_for_status() + + if is_stopped_callback and is_stopped_callback(): + raise Exception("Download stopped") - for chunk in response.iter_content(chunk_size=self.download_chunk_size): - if is_stopped_callback and is_stopped_callback(): - f.close() - if os.path.exists(temp_filename): - os.remove(temp_filename) - raise Exception("Download stopped") - - while is_paused_callback and is_paused_callback(): - time.sleep(0.1) - if is_stopped_callback and is_stopped_callback(): - f.close() - if os.path.exists(temp_filename): - os.remove(temp_filename) - 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) + 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") diff --git a/tidal.png b/tidal.png new file mode 100644 index 0000000..8d37f2b Binary files /dev/null and b/tidal.png differ diff --git a/tidalDL.py b/tidalDL.py index b90aa1e..0094001 100644 --- a/tidalDL.py +++ b/tidalDL.py @@ -3,7 +3,7 @@ import json import os import re import time -import httpx +import requests from mutagen.flac import FLAC, Picture from mutagen.id3 import PictureType @@ -35,7 +35,7 @@ class TidalDownloader: sanitized = re.sub(r'[\\/*?:"<>|]', "", str(filename)) 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" payload = { @@ -43,68 +43,67 @@ class TidalDownloader: "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: - 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: 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" header = {"authorization": f"Bearer {tidal_token}"} - async with httpx.AsyncClient(http2=True) as client: - search_data = await client.get(url=search_url, headers=header) - response_data = search_data.json() - - filtered_items = [{ - "id": item.get("id"), - "title": item.get("title"), - "url": item.get("url"), - "isrc": item.get("isrc"), - "audioQuality": item.get("audioQuality"), - "mediaMetadata": item.get("mediaMetadata"), - "album": item.get("album", {}), - "artists": item.get("artists", []), - "artist": item.get("artist", {}), - "trackNumber": item.get("trackNumber"), - "volumeNumber": item.get("volumeNumber"), - "duration": item.get("duration"), - "copyright": item.get("copyright"), - "explicit": item.get("explicit") - } for item in response_data.get("items", [])] - - return { - "limit": response_data.get("limit"), - "offset": response_data.get("offset"), - "totalNumberOfItems": response_data.get("totalNumberOfItems"), - "items": filtered_items - } + search_data = requests.get(url=search_url, headers=header, timeout=self.timeout) + response_data = search_data.json() + + filtered_items = [{ + "id": item.get("id"), + "title": item.get("title"), + "url": item.get("url"), + "isrc": item.get("isrc"), + "audioQuality": item.get("audioQuality"), + "mediaMetadata": item.get("mediaMetadata"), + "album": item.get("album", {}), + "artists": item.get("artists", []), + "artist": item.get("artist", {}), + "trackNumber": item.get("trackNumber"), + "volumeNumber": item.get("volumeNumber"), + "duration": item.get("duration"), + "copyright": item.get("copyright"), + "explicit": item.get("explicit") + } for item in response_data.get("items", [])] + + return { + "limit": response_data.get("limit"), + "offset": response_data.get("offset"), + "totalNumberOfItems": response_data.get("totalNumberOfItems"), + "items": filtered_items + } except Exception as 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 "")) try: - result = await self.search_tracks(query) + result = self.search_tracks(query) if not result or not result.get("items"): raise Exception(f"No tracks found for query: {query}") @@ -143,99 +142,73 @@ class TidalDownloader: except Exception as 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...") 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: - response = await client.get(download_api_url) + try: + response = requests.get(download_api_url, timeout=self.timeout) + + if response.status_code == 200: + data = response.json() - if response.status_code == 200: - data = response.json() - - for item in data: - if "OriginalTrackUrl" in item: - print("URL found") - 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}") - - except Exception as e: - raise Exception(f"Error getting download URL: {str(e)}") + for item in data: + if "OriginalTrackUrl" in item: + print("URL found") + 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}") + + 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: 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 = await client.get(art_url) + response = requests.get(art_url, timeout=self.timeout) + + 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: print(f"Error downloading album art: {str(e)}") 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" retry_count = 0 while retry_count <= self.max_retries: try: - async with httpx.AsyncClient(http2=True, timeout=60.0) as client: - async with client.stream('GET', url) as response: - if response.status_code != 200: - raise Exception(f"HTTP {response.status_code}") - - total_size = int(response.headers.get('content-length', 0)) - downloaded_size = 0 - start_time = time.time() - last_update_time = start_time - - with open(temp_filepath, 'wb') as f: - async for chunk in response.aiter_bytes(chunk_size=self.download_chunk_size): - if is_stopped_callback and is_stopped_callback(): - f.close() - if os.path.exists(temp_filepath): - os.remove(temp_filepath) - raise Exception("Download stopped") - - while is_paused_callback and is_paused_callback(): - 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) - + response = requests.get(url, timeout=60.0) + if response.status_code != 200: + raise Exception(f"HTTP {response.status_code}") + + 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_filepath, 'wb') as f: + f.write(response.content) + + downloaded_size = len(response.content) + + if self.progress_callback: + self.progress_callback(downloaded_size, downloaded_size) + os.rename(temp_filepath, filepath) print("Download complete") 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"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: print("Embedding metadata...") audio = FLAC(filepath) @@ -325,7 +298,7 @@ class TidalDownloader: audio["COMMENT"] = f"Tidal {track_info['audioQuality']}" 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: picture = Picture() picture.data = album_art @@ -343,14 +316,14 @@ class TidalDownloader: print(f"Error embedding metadata: {str(e)}") 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 != ".": try: os.makedirs(output_dir, exist_ok=True) except OSError as 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") if not track_id: @@ -376,12 +349,12 @@ class TidalDownloader: print(f"File already exists: {output_filename} ({file_size / (1024 * 1024):.2f} MB)") 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_track_info = download_info["track_info"] print(f"Downloading to: {output_filename}") - await self.download_file( + self.download_file( download_url, output_filename, is_paused_callback=is_paused_callback, @@ -390,7 +363,7 @@ class TidalDownloader: print("Adding metadata...") 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") except Exception as e: print(f"Tagging failed: {e}") @@ -398,7 +371,7 @@ class TidalDownloader: print("Done") return output_filename -async def main(): +def main(): print("=== TidalDL - Tidal Downloader ===") downloader = TidalDownloader(timeout=30, max_retries=3) @@ -407,7 +380,7 @@ async def main(): output_dir = "." 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}") except Exception as e: print(f"Error: {str(e)}") @@ -425,4 +398,4 @@ if __name__ == "__main__": except: pass - asyncio.run(main()) \ No newline at end of file + main() \ No newline at end of file diff --git a/us.svg b/us.svg new file mode 100644 index 0000000..9cfd0c9 --- /dev/null +++ b/us.svg @@ -0,0 +1,9 @@ + + + + + + + + +