From b4fcb6bca6ac204f583d37010806016e78550a3b Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Tue, 21 Oct 2025 02:11:41 +0700 Subject: [PATCH] v5.1 --- SpotiFLAC.py | 255 +++++++++++++++++++++++++++++++++++++-------------- tidalDL.py | 89 ++++++++++++++++-- 2 files changed, 267 insertions(+), 77 deletions(-) diff --git a/SpotiFLAC.py b/SpotiFLAC.py index 93147fe..488a99a 100644 --- a/SpotiFLAC.py +++ b/SpotiFLAC.py @@ -6,6 +6,7 @@ from pathlib import Path import requests import re import asyncio +import json from packaging import version import qdarktheme from mutagen.flac import FLAC @@ -83,7 +84,7 @@ class DownloadWorker(QThread): def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False, album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True, - use_artist_subfolders=False, use_album_subfolders=False, service="tidal"): + use_artist_subfolders=False, use_album_subfolders=False, service="tidal", tidal_api_url=None): super().__init__() self.tracks = tracks self.outpath = outpath @@ -96,6 +97,7 @@ class DownloadWorker(QThread): self.use_artist_subfolders = use_artist_subfolders self.use_album_subfolders = use_album_subfolders self.service = service + self.tidal_api_url = tidal_api_url self.is_paused = False self.is_stopped = False self.failed_tracks = [] @@ -123,11 +125,11 @@ class DownloadWorker(QThread): def run(self): try: if self.service == "tidal": - downloader = TidalDownloader() + downloader = TidalDownloader(api_url=self.tidal_api_url) elif self.service == "deezer": downloader = DeezerDownloader() else: - downloader = TidalDownloader() + downloader = TidalDownloader(api_url=self.tidal_api_url) def progress_update(current, total): if total <= 0: @@ -368,33 +370,7 @@ class UpdateDialog(QDialog): self.update_button.clicked.connect(self.accept) self.cancel_button.clicked.connect(self.reject) -class TidalStatusChecker(QThread): - status_updated = pyqtSignal(bool) - error = pyqtSignal(str) - - def run(self): - try: - response = requests.get("https://tidal.401658.xyz", timeout=5) - is_online = response.status_code == 200 or response.status_code == 429 - self.status_updated.emit(is_online) - except Exception as e: - self.error.emit(f"Error checking Tidal (API) status: {str(e)}") - self.status_updated.emit(False) - -class DeezerStatusChecker(QThread): - status_updated = pyqtSignal(bool) - error = pyqtSignal(str) - - def run(self): - try: - response = requests.get("https://deezmate.com/", timeout=5) - is_online = response.status_code == 200 - self.status_updated.emit(is_online) - except Exception as e: - self.error.emit(f"Error checking Deezer status: {str(e)}") - self.status_updated.emit(False) - -class StatusIndicatorDelegate(QStyledItemDelegate): +class ServiceStatusDelegate(QStyledItemDelegate): def paint(self, painter, option, index): item_data = index.data(Qt.ItemDataRole.UserRole) is_online = item_data.get('online', False) if item_data else False @@ -413,31 +389,75 @@ class StatusIndicatorDelegate(QStyledItemDelegate): painter.drawEllipse(circle_x, circle_y, circle_size, circle_size) painter.restore() +class TidalAPIDelegate(QStyledItemDelegate): + def paint(self, painter, option, index): + item_data = index.data(Qt.ItemDataRole.UserRole) + + super().paint(painter, option, index) + + if item_data and isinstance(item_data, dict) and 'status' in item_data: + is_online = item_data.get('status') == 'UP' + indicator_color = Qt.GlobalColor.green if is_online else Qt.GlobalColor.red + + circle_size = 6 + circle_y = option.rect.center().y() - circle_size // 2 + circle_x = option.rect.right() - circle_size - 5 + + painter.save() + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QBrush(indicator_color)) + painter.drawEllipse(circle_x, circle_y, circle_size, circle_size) + painter.restore() + +class TidalStatusChecker(QThread): + status_updated = pyqtSignal(bool) + error = pyqtSignal(str) + + def run(self): + try: + response = requests.get("https://status.monochrome.tf", timeout=5) + is_online = response.status_code == 200 + self.status_updated.emit(is_online) + except Exception as e: + self.error.emit(f"Error checking Tidal status: {str(e)}") + self.status_updated.emit(False) + +class DeezerStatusChecker(QThread): + status_updated = pyqtSignal(bool) + error = pyqtSignal(str) + + def run(self): + try: + response = requests.get("https://deezmate.com/", timeout=5) + is_online = response.status_code == 200 + self.status_updated.emit(is_online) + except Exception as e: + self.error.emit(f"Error checking Deezer status: {str(e)}") + self.status_updated.emit(False) + class ServiceComboBox(QComboBox): def __init__(self, parent=None): super().__init__(parent) self.setIconSize(QSize(16, 16)) - self.services_status = {} - - self.setItemDelegate(StatusIndicatorDelegate()) + self.setItemDelegate(ServiceStatusDelegate()) self.setup_items() 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.status_updated.connect(self.update_tidal_status) + self.tidal_status_checker.error.connect(lambda e: print(f"Tidal status check error: {e}")) self.tidal_status_checker.start() self.tidal_status_timer = QTimer(self) - self.tidal_status_timer.timeout.connect(self.refresh_tidal_status) - self.tidal_status_timer.start(60000) + self.tidal_status_timer.timeout.connect(self.refresh_tidal_status) + self.tidal_status_timer.start(60000) 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}")) + self.deezer_status_checker.status_updated.connect(self.update_deezer_status) + self.deezer_status_checker.error.connect(lambda e: print(f"Deezer status check error: {e}")) self.deezer_status_checker.start() 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(60000) def setup_items(self): @@ -463,42 +483,47 @@ class ServiceComboBox(QComboBox): pixmap = QPixmap(16, 16) pixmap.fill(Qt.GlobalColor.transparent) pixmap.save(path) - - def update_service_status(self, service_id, is_online): + + def update_tidal_status(self, is_online): for i in range(self.count()): - current_service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) - if current_service_id == service_id: + service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) + if service_id == 'tidal': 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 + 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 = TidalStatusChecker() + self.tidal_status_checker.status_updated.connect(self.update_tidal_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): - self.update_service_status('deezer', is_online) - + + def update_deezer_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() + 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}")) + + self.deezer_status_checker = DeezerStatusChecker() + self.deezer_status_checker.status_updated.connect(self.update_deezer_status) + self.deezer_status_checker.error.connect(lambda e: print(f"Deezer status check error: {e}")) self.deezer_status_checker.start() def currentData(self, role=Qt.ItemDataRole.UserRole + 1): @@ -507,7 +532,7 @@ class ServiceComboBox(QComboBox): class SpotiFLACGUI(QWidget): def __init__(self): super().__init__() - self.current_version = "5.0" + self.current_version = "5.1" self.tracks = [] self.all_tracks = [] self.successful_downloads = [] @@ -522,6 +547,7 @@ class SpotiFLACGUI(QWidget): self.use_artist_subfolders = self.settings.value('use_artist_subfolders', False, type=bool) self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool) self.service = self.settings.value('service', 'tidal') + self.tidal_api = self.settings.value('tidal_api', 'https://hifi.401658.xyz') self.check_for_updates = self.settings.value('check_for_updates', True, type=bool) self.current_theme_color = self.settings.value('theme_color', '#2196F3') self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration') @@ -1109,17 +1135,44 @@ class SpotiFLACGUI(QWidget): auth_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;") auth_layout.addWidget(auth_label) - service_fallback_layout = QHBoxLayout() + service_api_layout = QHBoxLayout() service_label = QLabel('Service:') + service_label.setFixedWidth(60) self.service_dropdown = ServiceComboBox() + self.service_dropdown.setFixedWidth(120) self.service_dropdown.currentIndexChanged.connect(self.on_service_changed) - service_fallback_layout.addWidget(service_label) - service_fallback_layout.addWidget(self.service_dropdown) - service_fallback_layout.addStretch() - auth_layout.addLayout(service_fallback_layout) + service_api_layout.addWidget(service_label) + service_api_layout.addWidget(self.service_dropdown) + service_api_layout.addSpacing(15) + + self.tidal_api_label = QLabel('Tidal API:') + self.tidal_api_label.setFixedWidth(70) + + self.tidal_api_dropdown = QComboBox() + self.tidal_api_dropdown.setItemDelegate(TidalAPIDelegate()) + self.tidal_api_dropdown.addItem("Default (401658)", "https://hifi.401658.xyz") + self.tidal_api_dropdown.addItem("Auto-select fastest", "auto") + self.tidal_api_dropdown.currentIndexChanged.connect(self.on_tidal_api_changed) + + self.refresh_api_btn = QPushButton('Refresh') + self.refresh_api_btn.setFixedWidth(80) + self.refresh_api_btn.setCursor(Qt.CursorShape.PointingHandCursor) + self.refresh_api_btn.clicked.connect(self.refresh_tidal_apis) + + service_api_layout.addWidget(self.tidal_api_label) + service_api_layout.addWidget(self.tidal_api_dropdown) + service_api_layout.addSpacing(5) + service_api_layout.addWidget(self.refresh_api_btn) + service_api_layout.addStretch() + + auth_layout.addLayout(service_api_layout) + + self.refresh_tidal_apis() + + self.update_tidal_api_visibility() settings_layout.addWidget(auth_group) settings_layout.addStretch() @@ -1129,6 +1182,8 @@ class SpotiFLACGUI(QWidget): self.set_combobox_value(self.track_list_format_dropdown, self.track_list_format) self.set_combobox_value(self.date_format_dropdown, self.date_format) + self.set_combobox_value(self.tidal_api_dropdown, self.tidal_api) + def setup_theme_tab(self): theme_tab = QWidget() theme_layout = QVBoxLayout() @@ -1361,6 +1416,55 @@ class SpotiFLACGUI(QWidget): self.settings.setValue('service', service) self.settings.sync() self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}") + self.update_tidal_api_visibility() + + def update_tidal_api_visibility(self): + is_tidal = self.service_dropdown.currentData() == 'tidal' + self.tidal_api_label.setVisible(is_tidal) + self.tidal_api_dropdown.setVisible(is_tidal) + self.refresh_api_btn.setVisible(is_tidal) + + def on_tidal_api_changed(self, index): + selected_api = self.tidal_api_dropdown.currentData() + if selected_api: + self.tidal_api = selected_api + self.settings.setValue('tidal_api', selected_api) + self.settings.sync() + self.log_output.append(f"Tidal API changed to: {self.tidal_api_dropdown.currentText()}") + + def refresh_tidal_apis(self): + try: + self.log_output.append("Fetching available Tidal APIs...") + apis = TidalDownloader.get_available_apis() + + while self.tidal_api_dropdown.count() > 2: + self.tidal_api_dropdown.removeItem(2) + + if apis: + for api in apis: + url = api.get('url', '') + uptime = api.get('uptime', 0) + avg_time = api.get('avg_response_time', 0) + status = "UP" if api.get('last_check', {}).get('success') else "DOWN" + + domain = url.replace('https://', '').replace('http://', '') + label = f"{domain} ({uptime:.0f}%, {avg_time}ms)" + + status_data = { + 'status': status, + 'uptime': uptime, + 'avg_time': avg_time + } + + self.tidal_api_dropdown.addItem(label, url) + item_index = self.tidal_api_dropdown.count() - 1 + self.tidal_api_dropdown.setItemData(item_index, status_data, Qt.ItemDataRole.UserRole) + + self.log_output.append(f"Found {len(apis)} available Tidal APIs") + else: + self.log_output.append("No APIs found, using default") + except Exception as e: + self.log_output.append(f"Error fetching APIs: {str(e)}") def save_url(self): self.settings.setValue('spotify_url', self.spotify_url.text().strip()) @@ -1818,6 +1922,20 @@ class SpotiFLACGUI(QWidget): def start_download_worker(self, tracks_to_download, outpath): service = self.service_dropdown.currentData() + tidal_api_url = None + if service == "tidal": + selected_api = self.tidal_api_dropdown.currentData() + if selected_api == "auto": + apis = TidalDownloader.get_available_apis() + if apis: + tidal_api_url = apis[0]['url'] + self.log_output.append(f"Auto-selected fastest API: {tidal_api_url}") + else: + tidal_api_url = "https://hifi.401658.xyz" + self.log_output.append("Using default API: https://hifi.401658.xyz") + else: + tidal_api_url = selected_api + self.worker = DownloadWorker( tracks_to_download, outpath, @@ -1829,7 +1947,8 @@ class SpotiFLACGUI(QWidget): self.use_track_numbers, self.use_artist_subfolders, self.use_album_subfolders, - service + service, + tidal_api_url ) self.worker.finished.connect(lambda success, message, failed_tracks, successful_tracks, skipped_tracks: self.on_download_finished(success, message, failed_tracks, successful_tracks, skipped_tracks)) self.worker.progress.connect(self.update_progress) diff --git a/tidalDL.py b/tidalDL.py index d0772e5..8d7423d 100644 --- a/tidalDL.py +++ b/tidalDL.py @@ -1,9 +1,9 @@ -import asyncio -import json import os import re import time +import base64 import requests +import json from mutagen.flac import FLAC, Picture from mutagen.id3 import PictureType @@ -16,19 +16,88 @@ class ProgressCallback: print(f"\r{current / (1024 * 1024):.2f} MB", end="") class TidalDownloader: - def __init__(self, timeout=30, max_retries=3): + def __init__(self, timeout=30, max_retries=3, api_url=None): self.timeout = timeout self.max_retries = max_retries self.download_chunk_size = 256 * 1024 self.progress_callback = ProgressCallback() - self.client_id = "zU4XHVVkc2tDPo4t" - self.client_secret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=" + self.client_id = base64.b64decode("NkJEU1JkcEs5aHFFQlRnVQ==").decode() + self.client_secret = base64.b64decode("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=").decode() + self.api_url = api_url or "https://hifi.401658.xyz" + + @staticmethod + def get_available_apis(): + try: + response = requests.get("https://status.monochrome.tf/api/stream", timeout=10, stream=True) + + for line in response.iter_lines(): + if line: + line_str = line.decode('utf-8') + if line_str.startswith('data: '): + data = json.loads(line_str[6:]) + + api_instances = [ + inst for inst in data.get('instances', []) + if inst.get('instance_type') == 'api' and inst.get('last_check', {}).get('success') + ] + + api_instances.sort(key=lambda x: x.get('avg_response_time', 9999)) + + return api_instances + + except Exception as e: + print(f"Failed to fetch API list: {e}") + return [] + + @staticmethod + def select_api_interactive(): + apis = TidalDownloader.get_available_apis() + + if not apis: + print("No APIs available, using default: https://hifi.401658.xyz") + return "https://hifi.401658.xyz" + + print("\n=== Available Tidal APIs ===") + print(f"{'No':<4} {'URL':<40} {'Status':<8} {'Uptime':<8} {'Avg Response':<12}") + print("-" * 80) + + for i, api in enumerate(apis, 1): + url = api.get('url', 'N/A') + status = "UP" if api.get('last_check', {}).get('success') else "DOWN" + uptime = f"{api.get('uptime', 0):.1f}%" + avg_time = f"{api.get('avg_response_time', 0)}ms" + + print(f"{i:<4} {url:<40} {status:<8} {uptime:<8} {avg_time:<12}") + + print("\n0 Use default (https://hifi.401658.xyz)") + print("-" * 80) + + while True: + try: + choice = input(f"\nSelect API (0-{len(apis)}) [1 for fastest]: ").strip() + + if not choice: + choice = "1" + + choice_num = int(choice) + + if choice_num == 0: + return "https://hifi.401658.xyz" + elif 1 <= choice_num <= len(apis): + selected_url = apis[choice_num - 1]['url'] + print(f"\nSelected: {selected_url}") + return selected_url + else: + print(f"Invalid choice. Please enter 0-{len(apis)}") + except ValueError: + print("Invalid input. Please enter a number.") + except KeyboardInterrupt: + print("\nUsing default API") + return "https://hifi.401658.xyz" def set_progress_callback(self, callback): self.progress_callback = callback - - def sanitize_filename(self, filename): if not filename: return "Unknown Track" @@ -144,7 +213,7 @@ class TidalDownloader: def get_download_url(self, track_id, quality="LOSSLESS"): print("Fetching URL...") - download_api_url = f"https://tidal.401658.xyz/track/?id={track_id}&quality={quality}" + download_api_url = f"{self.api_url}/track/?id={track_id}&quality={quality}" try: response = requests.get(download_api_url, timeout=self.timeout) @@ -373,7 +442,9 @@ class TidalDownloader: def main(): print("=== TidalDL - Tidal Downloader ===") - downloader = TidalDownloader(timeout=30, max_retries=3) + + selected_api = TidalDownloader.select_api_interactive() + downloader = TidalDownloader(timeout=30, max_retries=3, api_url=selected_api) query = "APT." isrc = "USAT22409172"