This commit is contained in:
afkarxyz
2025-10-21 02:11:41 +07:00
parent 5ab19a6d37
commit b4fcb6bca6
2 changed files with 267 additions and 77 deletions
+171 -52
View File
@@ -6,6 +6,7 @@ from pathlib import Path
import requests import requests
import re import re
import asyncio import asyncio
import json
from packaging import version from packaging import version
import qdarktheme import qdarktheme
from mutagen.flac import FLAC 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, def __init__(self, tracks, outpath, is_single_track=False, is_album=False, is_playlist=False,
album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True, album_or_playlist_name='', filename_format='title_artist', use_track_numbers=True,
use_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__() super().__init__()
self.tracks = tracks self.tracks = tracks
self.outpath = outpath self.outpath = outpath
@@ -96,6 +97,7 @@ class DownloadWorker(QThread):
self.use_artist_subfolders = use_artist_subfolders self.use_artist_subfolders = use_artist_subfolders
self.use_album_subfolders = use_album_subfolders self.use_album_subfolders = use_album_subfolders
self.service = service self.service = service
self.tidal_api_url = tidal_api_url
self.is_paused = False self.is_paused = False
self.is_stopped = False self.is_stopped = False
self.failed_tracks = [] self.failed_tracks = []
@@ -123,11 +125,11 @@ class DownloadWorker(QThread):
def run(self): def run(self):
try: try:
if self.service == "tidal": if self.service == "tidal":
downloader = TidalDownloader() downloader = TidalDownloader(api_url=self.tidal_api_url)
elif self.service == "deezer": elif self.service == "deezer":
downloader = DeezerDownloader() downloader = DeezerDownloader()
else: else:
downloader = TidalDownloader() downloader = TidalDownloader(api_url=self.tidal_api_url)
def progress_update(current, total): def progress_update(current, total):
if total <= 0: if total <= 0:
@@ -368,33 +370,7 @@ 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 ServiceStatusDelegate(QStyledItemDelegate):
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):
def paint(self, painter, option, index): def paint(self, painter, option, index):
item_data = index.data(Qt.ItemDataRole.UserRole) item_data = index.data(Qt.ItemDataRole.UserRole)
is_online = item_data.get('online', False) if item_data else False is_online = item_data.get('online', False) if item_data else False
@@ -413,17 +389,61 @@ class StatusIndicatorDelegate(QStyledItemDelegate):
painter.drawEllipse(circle_x, circle_y, circle_size, circle_size) painter.drawEllipse(circle_x, circle_y, circle_size, circle_size)
painter.restore() 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): class ServiceComboBox(QComboBox):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setIconSize(QSize(16, 16)) self.setIconSize(QSize(16, 16))
self.services_status = {} self.setItemDelegate(ServiceStatusDelegate())
self.setItemDelegate(StatusIndicatorDelegate())
self.setup_items() self.setup_items()
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_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()
@@ -432,7 +452,7 @@ class ServiceComboBox(QComboBox):
self.tidal_status_timer.start(60000) 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_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}"))
self.deezer_status_checker.start() self.deezer_status_checker.start()
@@ -464,10 +484,10 @@ class ServiceComboBox(QComboBox):
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)
pixmap.save(path) 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()): for i in range(self.count()):
current_service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1) service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1)
if current_service_id == service_id: if service_id == 'tidal':
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
@@ -475,21 +495,26 @@ 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(): if hasattr(self, 'tidal_status_checker') and self.tidal_status_checker.isRunning():
self.tidal_status_checker.quit() self.tidal_status_checker.quit()
self.tidal_status_checker.wait() 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_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_status(self, is_online):
self.update_service_status('deezer', 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): def refresh_deezer_status(self):
if hasattr(self, 'deezer_status_checker') and self.deezer_status_checker.isRunning(): if hasattr(self, 'deezer_status_checker') and self.deezer_status_checker.isRunning():
@@ -497,7 +522,7 @@ class ServiceComboBox(QComboBox):
self.deezer_status_checker.wait() 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_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}"))
self.deezer_status_checker.start() self.deezer_status_checker.start()
@@ -507,7 +532,7 @@ class ServiceComboBox(QComboBox):
class SpotiFLACGUI(QWidget): class SpotiFLACGUI(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "5.0" self.current_version = "5.1"
self.tracks = [] self.tracks = []
self.all_tracks = [] self.all_tracks = []
self.successful_downloads = [] 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_artist_subfolders = self.settings.value('use_artist_subfolders', False, type=bool)
self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool) self.use_album_subfolders = self.settings.value('use_album_subfolders', False, type=bool)
self.service = self.settings.value('service', 'tidal') self.service = self.settings.value('service', 'tidal')
self.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.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
self.current_theme_color = self.settings.value('theme_color', '#2196F3') self.current_theme_color = self.settings.value('theme_color', '#2196F3')
self.track_list_format = self.settings.value('track_list_format', 'track_artist_date_duration') self.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_label.setStyleSheet("font-weight: bold; margin-top: 8px; margin-bottom: 5px;")
auth_layout.addWidget(auth_label) auth_layout.addWidget(auth_label)
service_fallback_layout = QHBoxLayout() service_api_layout = QHBoxLayout()
service_label = QLabel('Service:') service_label = QLabel('Service:')
service_label.setFixedWidth(60)
self.service_dropdown = ServiceComboBox() self.service_dropdown = ServiceComboBox()
self.service_dropdown.setFixedWidth(120)
self.service_dropdown.currentIndexChanged.connect(self.on_service_changed) 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() service_api_layout.addWidget(service_label)
auth_layout.addLayout(service_fallback_layout) 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.addWidget(auth_group)
settings_layout.addStretch() 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.track_list_format_dropdown, self.track_list_format)
self.set_combobox_value(self.date_format_dropdown, self.date_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): def setup_theme_tab(self):
theme_tab = QWidget() theme_tab = QWidget()
theme_layout = QVBoxLayout() theme_layout = QVBoxLayout()
@@ -1361,6 +1416,55 @@ class SpotiFLACGUI(QWidget):
self.settings.setValue('service', service) self.settings.setValue('service', service)
self.settings.sync() self.settings.sync()
self.log_output.append(f"Service changed to: {self.service_dropdown.currentText()}") 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): def save_url(self):
self.settings.setValue('spotify_url', self.spotify_url.text().strip()) 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): def start_download_worker(self, tracks_to_download, outpath):
service = self.service_dropdown.currentData() 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( self.worker = DownloadWorker(
tracks_to_download, tracks_to_download,
outpath, outpath,
@@ -1829,7 +1947,8 @@ class SpotiFLACGUI(QWidget):
self.use_track_numbers, self.use_track_numbers,
self.use_artist_subfolders, self.use_artist_subfolders,
self.use_album_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.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) self.worker.progress.connect(self.update_progress)
+80 -9
View File
@@ -1,9 +1,9 @@
import asyncio
import json
import os import os
import re import re
import time import time
import base64
import requests import requests
import json
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType from mutagen.id3 import PictureType
@@ -16,19 +16,88 @@ class ProgressCallback:
print(f"\r{current / (1024 * 1024):.2f} MB", end="") print(f"\r{current / (1024 * 1024):.2f} MB", end="")
class TidalDownloader: 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.timeout = timeout
self.max_retries = max_retries self.max_retries = max_retries
self.download_chunk_size = 256 * 1024 self.download_chunk_size = 256 * 1024
self.progress_callback = ProgressCallback() self.progress_callback = ProgressCallback()
self.client_id = "zU4XHVVkc2tDPo4t" self.client_id = base64.b64decode("NkJEU1JkcEs5aHFFQlRnVQ==").decode()
self.client_secret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=" 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): def set_progress_callback(self, callback):
self.progress_callback = callback self.progress_callback = callback
def sanitize_filename(self, filename): def sanitize_filename(self, filename):
if not filename: if not filename:
return "Unknown Track" return "Unknown Track"
@@ -144,7 +213,7 @@ class TidalDownloader:
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://tidal.401658.xyz/track/?id={track_id}&quality={quality}" download_api_url = f"{self.api_url}/track/?id={track_id}&quality={quality}"
try: try:
response = requests.get(download_api_url, timeout=self.timeout) response = requests.get(download_api_url, timeout=self.timeout)
@@ -373,7 +442,9 @@ class TidalDownloader:
def main(): def main():
print("=== TidalDL - Tidal Downloader ===") 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." query = "APT."
isrc = "USAT22409172" isrc = "USAT22409172"