This commit is contained in:
afkarxyz
2025-11-17 12:48:04 +07:00
parent fd949a17f0
commit 711c5a98d3
2 changed files with 180 additions and 71 deletions
+154 -31
View File
@@ -126,10 +126,13 @@ class DownloadWorker(QThread):
try: try:
if self.service == "tidal": if self.service == "tidal":
downloader = TidalDownloader(api_url=self.tidal_api_url) downloader = TidalDownloader(api_url=self.tidal_api_url)
deezer_downloader = DeezerDownloader()
elif self.service == "deezer": elif self.service == "deezer":
downloader = DeezerDownloader() downloader = DeezerDownloader()
deezer_downloader = None
else: else:
downloader = TidalDownloader(api_url=self.tidal_api_url) downloader = TidalDownloader(api_url=self.tidal_api_url)
deezer_downloader = DeezerDownloader()
def progress_update(current, total): def progress_update(current, total):
if total <= 0: if total <= 0:
@@ -316,6 +319,46 @@ class DownloadWorker(QThread):
int((i + 1) / total_tracks * 100)) int((i + 1) / total_tracks * 100))
self.successful_tracks.append(track) self.successful_tracks.append(track)
except Exception as e: except Exception as e:
if self.service == "tidal" and deezer_downloader and track.isrc:
try:
self.progress.emit(f"Tidal failed, trying Deezer fallback for: {track.title}", 0)
success = asyncio.run(deezer_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()
safe_artist = "".join(c for c in track.artists if c.isalnum() or c in (' ', '-', '_')).rstrip()
expected_filename = f"{safe_artist} - {safe_title}.flac"
downloaded_file = os.path.join(track_outpath, expected_filename)
if not os.path.exists(downloaded_file):
import glob
flac_files = glob.glob(os.path.join(track_outpath, "*.flac"))
if flac_files:
downloaded_file = max(flac_files, key=os.path.getctime)
else:
raise Exception("Downloaded file not found")
if downloaded_file != new_filepath:
try:
os.rename(downloaded_file, new_filepath)
self.progress.emit(f"File renamed to: {new_filename}", 0)
except OSError:
pass
self.progress.emit(f"Successfully downloaded via Deezer fallback: {track.title} - {track.artists}",
int((i + 1) / total_tracks * 100))
self.successful_tracks.append(track)
continue
else:
raise Exception("Deezer fallback also failed")
except Exception as deezer_error:
self.progress.emit(f"Deezer fallback also failed: {str(deezer_error)}", 0)
self.failed_tracks.append((track.title, track.artists, f"Tidal: {str(e)}, Deezer: {str(deezer_error)}"))
self.progress.emit(f"Failed to download: {track.title} - {track.artists}\nBoth services failed",
int((i + 1) / total_tracks * 100))
continue
self.failed_tracks.append((track.title, track.artists, str(e))) self.failed_tracks.append((track.title, track.artists, str(e)))
self.progress.emit(f"Failed to download: {track.title} - {track.artists}\nError: {str(e)}", self.progress.emit(f"Failed to download: {track.title} - {track.artists}\nError: {str(e)}",
int((i + 1) / total_tracks * 100)) int((i + 1) / total_tracks * 100))
@@ -418,11 +461,39 @@ class TidalStatusChecker(QThread):
status_updated = pyqtSignal(bool) status_updated = pyqtSignal(bool)
error = pyqtSignal(str) error = pyqtSignal(str)
def check_single_api(self, api):
try:
url = api.get('url', '')
test_response = requests.get(f"{url}/track/?id=251380837&quality=LOSSLESS", timeout=5)
return test_response.status_code == 200
except:
return False
def run(self): def run(self):
try: try:
response = requests.get("https://status.monochrome.tf", timeout=5) from concurrent.futures import ThreadPoolExecutor, as_completed
is_online = response.status_code == 200
self.status_updated.emit(is_online) apis = TidalDownloader.get_available_apis()
if not apis:
self.status_updated.emit(False)
return
any_online = False
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(self.check_single_api, api): api for api in apis}
for future in as_completed(futures):
try:
if future.result():
any_online = True
for f in futures:
f.cancel()
break
except:
continue
self.status_updated.emit(any_online)
except Exception as e: except Exception as e:
self.error.emit(f"Error checking Tidal status: {str(e)}") self.error.emit(f"Error checking Tidal status: {str(e)}")
self.status_updated.emit(False) self.status_updated.emit(False)
@@ -440,6 +511,39 @@ class DeezerStatusChecker(QThread):
self.error.emit(f"Error checking Deezer status: {str(e)}") self.error.emit(f"Error checking Deezer status: {str(e)}")
self.status_updated.emit(False) self.status_updated.emit(False)
class APIStatusChecker(QThread):
status_checked = pyqtSignal(str, str)
all_completed = pyqtSignal()
def __init__(self, apis):
super().__init__()
self.apis = apis
def check_single_api(self, api):
url = api.get('url', '')
try:
test_response = requests.get(f"{url}/track/?id=251380837&quality=LOSSLESS", timeout=5)
is_online = test_response.status_code == 200
status = 'UP' if is_online else 'DOWN'
except:
status = 'DOWN'
return (url, status)
def run(self):
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(self.check_single_api, api): api for api in self.apis}
for future in as_completed(futures):
try:
url, status = future.result()
self.status_checked.emit(url, status)
except Exception as e:
print(f"Error checking API: {e}")
self.all_completed.emit()
class ServiceComboBox(QComboBox): class ServiceComboBox(QComboBox):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -447,24 +551,29 @@ class ServiceComboBox(QComboBox):
self.setItemDelegate(ServiceStatusDelegate()) self.setItemDelegate(ServiceStatusDelegate())
self.setup_items() self.setup_items()
self.tidal_status_checker = TidalStatusChecker() QTimer.singleShot(100, self.start_tidal_status_check)
self.tidal_status_checker.status_updated.connect(self.update_tidal_status) QTimer.singleShot(100, self.start_deezer_status_check)
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 = 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(60000) self.tidal_status_timer.start(60000)
self.deezer_status_timer = QTimer(self)
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status)
self.deezer_status_timer.start(60000)
def start_tidal_status_check(self):
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 start_deezer_status_check(self):
self.deezer_status_checker = DeezerStatusChecker() self.deezer_status_checker = DeezerStatusChecker()
self.deezer_status_checker.status_updated.connect(self.update_deezer_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()
self.deezer_status_timer = QTimer(self)
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status)
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__))
@@ -537,7 +646,7 @@ class ServiceComboBox(QComboBox):
class SpotiFLACGUI(QWidget): class SpotiFLACGUI(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "5.3" self.current_version = "5.4"
self.tracks = [] self.tracks = []
self.all_tracks = [] self.all_tracks = []
self.successful_downloads = [] self.successful_downloads = []
@@ -552,7 +661,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.tidal_api = self.settings.value('tidal_api', 'auto')
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')
@@ -1151,14 +1260,16 @@ class SpotiFLACGUI(QWidget):
service_api_layout.addWidget(service_label) service_api_layout.addWidget(service_label)
service_api_layout.addWidget(self.service_dropdown) service_api_layout.addWidget(self.service_dropdown)
service_api_layout.addSpacing(15)
self.api_spacer = QWidget()
self.api_spacer.setFixedWidth(15)
service_api_layout.addWidget(self.api_spacer)
self.tidal_api_label = QLabel('API Instances:') self.tidal_api_label = QLabel('API Instances:')
self.tidal_api_label.setFixedWidth(85) self.tidal_api_label.setFixedWidth(85)
self.tidal_api_dropdown = QComboBox() self.tidal_api_dropdown = QComboBox()
self.tidal_api_dropdown.setItemDelegate(TidalAPIDelegate()) self.tidal_api_dropdown.setItemDelegate(TidalAPIDelegate())
self.tidal_api_dropdown.addItem("Default", "https://hifi.401658.xyz")
self.tidal_api_dropdown.addItem("Auto Fallback", "auto") self.tidal_api_dropdown.addItem("Auto Fallback", "auto")
self.tidal_api_dropdown.currentIndexChanged.connect(self.on_tidal_api_changed) self.tidal_api_dropdown.currentIndexChanged.connect(self.on_tidal_api_changed)
@@ -1168,9 +1279,10 @@ class SpotiFLACGUI(QWidget):
self.refresh_api_btn.clicked.connect(self.refresh_tidal_apis) 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_label)
service_api_layout.addWidget(self.tidal_api_dropdown, 1) service_api_layout.addWidget(self.tidal_api_dropdown, 2)
service_api_layout.addSpacing(5) service_api_layout.addSpacing(5)
service_api_layout.addWidget(self.refresh_api_btn) service_api_layout.addWidget(self.refresh_api_btn)
service_api_layout.addStretch(1)
auth_layout.addLayout(service_api_layout) auth_layout.addLayout(service_api_layout)
@@ -1408,7 +1520,7 @@ class SpotiFLACGUI(QWidget):
about_layout.addWidget(section_widget) about_layout.addWidget(section_widget)
footer_label = QLabel(f"v{self.current_version} | October 2025") footer_label = QLabel(f"v{self.current_version} | November 2025")
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)
@@ -1424,6 +1536,7 @@ class SpotiFLACGUI(QWidget):
def update_tidal_api_visibility(self): def update_tidal_api_visibility(self):
is_tidal = self.service_dropdown.currentData() == 'tidal' is_tidal = self.service_dropdown.currentData() == 'tidal'
self.api_spacer.setVisible(is_tidal)
self.tidal_api_label.setVisible(is_tidal) self.tidal_api_label.setVisible(is_tidal)
self.tidal_api_dropdown.setVisible(is_tidal) self.tidal_api_dropdown.setVisible(is_tidal)
self.refresh_api_btn.setVisible(is_tidal) self.refresh_api_btn.setVisible(is_tidal)
@@ -1441,35 +1554,45 @@ class SpotiFLACGUI(QWidget):
self.log_output.append("Fetching available API instances...") self.log_output.append("Fetching available API instances...")
apis = TidalDownloader.get_available_apis() apis = TidalDownloader.get_available_apis()
while self.tidal_api_dropdown.count() > 2: while self.tidal_api_dropdown.count() > 1:
self.tidal_api_dropdown.removeItem(2) self.tidal_api_dropdown.removeItem(1)
if apis: if apis:
self.log_output.append(f"Found {len(apis)} API instances, loading...")
for api in apis: for api in apis:
url = api.get('url', '') 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://', '') domain = url.replace('https://', '').replace('http://', '')
label = f"{domain} ({uptime:.0f}%, {avg_time}ms)" label = domain
status_data = { status_data = {'status': 'CHECKING'}
'status': status,
'uptime': uptime,
'avg_time': avg_time
}
self.tidal_api_dropdown.addItem(label, url) self.tidal_api_dropdown.addItem(label, url)
item_index = self.tidal_api_dropdown.count() - 1 item_index = self.tidal_api_dropdown.count() - 1
self.tidal_api_dropdown.setItemData(item_index, status_data, Qt.ItemDataRole.UserRole + 1) self.tidal_api_dropdown.setItemData(item_index, status_data, Qt.ItemDataRole.UserRole + 1)
self.log_output.append(f"Found {len(apis)} available API instances") self.log_output.append(f"Loaded {len(apis)} API instances, checking status in background...")
self.api_status_checker = APIStatusChecker(apis)
self.api_status_checker.status_checked.connect(self.on_api_status_checked)
self.api_status_checker.all_completed.connect(self.on_all_api_status_completed)
self.api_status_checker.start()
else: else:
self.log_output.append("No APIs found, using default") self.log_output.append("No APIs found")
except Exception as e: except Exception as e:
self.log_output.append(f"Error fetching APIs: {str(e)}") self.log_output.append(f"Error fetching APIs: {str(e)}")
def on_api_status_checked(self, url, status):
for i in range(self.tidal_api_dropdown.count()):
if self.tidal_api_dropdown.itemData(i) == url:
status_data = {'status': status}
self.tidal_api_dropdown.setItemData(i, status_data, Qt.ItemDataRole.UserRole + 1)
break
self.tidal_api_dropdown.update()
def on_all_api_status_completed(self):
self.log_output.append("API status check completed")
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())
self.settings.sync() self.settings.sync()
+19 -33
View File
@@ -23,27 +23,22 @@ class TidalDownloader:
self.progress_callback = ProgressCallback() self.progress_callback = ProgressCallback()
self.client_id = base64.b64decode("NkJEU1JkcEs5aHFFQlRnVQ==").decode() self.client_id = base64.b64decode("NkJEU1JkcEs5aHFFQlRnVQ==").decode()
self.client_secret = base64.b64decode("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=").decode() self.client_secret = base64.b64decode("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=").decode()
self.api_url = api_url or "https://hifi.401658.xyz" self.api_url = api_url
@staticmethod @staticmethod
def get_available_apis(): def get_available_apis():
try: try:
response = requests.get("https://status.monochrome.tf/api/stream", timeout=10, stream=True) response = requests.get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/tidal.json", timeout=10)
for line in response.iter_lines(): if response.status_code == 200:
if line: api_list = response.json()
line_str = line.decode('utf-8')
if line_str.startswith('data: '):
data = json.loads(line_str[6:])
api_instances = [ api_instances = [{"url": f"https://{api}"} for api in api_list]
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 return api_instances
else:
print(f"Failed to fetch API list: HTTP {response.status_code}")
return []
except Exception as e: except Exception as e:
print(f"Failed to fetch API list: {e}") print(f"Failed to fetch API list: {e}")
@@ -54,46 +49,38 @@ class TidalDownloader:
apis = TidalDownloader.get_available_apis() apis = TidalDownloader.get_available_apis()
if not apis: if not apis:
print("No APIs available, using default: https://hifi.401658.xyz") raise Exception("No APIs available. Cannot proceed.")
return "https://hifi.401658.xyz"
print("\n=== Available API Instances ===") print("\n=== Available API Instances ===")
print(f"{'No':<4} {'URL':<40} {'Status':<8} {'Uptime':<8} {'Avg Response':<12}") print(f"{'No':<4} {'URL':<50}")
print("-" * 80) print("-" * 60)
for i, api in enumerate(apis, 1): for i, api in enumerate(apis, 1):
url = api.get('url', 'N/A') url = api.get('url', 'N/A')
status = "UP" if api.get('last_check', {}).get('success') else "DOWN" print(f"{i:<4} {url:<50}")
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("-" * 60)
print("\n0 Use default (https://hifi.401658.xyz)")
print("-" * 80)
while True: while True:
try: try:
choice = input(f"\nSelect API (0-{len(apis)}) [1 for fastest]: ").strip() choice = input(f"\nSelect API (1-{len(apis)}) [1]: ").strip()
if not choice: if not choice:
choice = "1" choice = "1"
choice_num = int(choice) choice_num = int(choice)
if choice_num == 0: if 1 <= choice_num <= len(apis):
return "https://hifi.401658.xyz"
elif 1 <= choice_num <= len(apis):
selected_url = apis[choice_num - 1]['url'] selected_url = apis[choice_num - 1]['url']
print(f"\nSelected: {selected_url}") print(f"\nSelected: {selected_url}")
return selected_url return selected_url
else: else:
print(f"Invalid choice. Please enter 0-{len(apis)}") print(f"Invalid choice. Please enter 1-{len(apis)}")
except ValueError: except ValueError:
print("Invalid input. Please enter a number.") print("Invalid input. Please enter a number.")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nUsing default API") print("\nCancelled")
return "https://hifi.401658.xyz" raise Exception("API selection cancelled")
def set_progress_callback(self, callback): def set_progress_callback(self, callback):
self.progress_callback = callback self.progress_callback = callback
@@ -399,8 +386,7 @@ class TidalDownloader:
if auto_fallback: if auto_fallback:
apis = self.get_available_apis() apis = self.get_available_apis()
if not apis: if not apis:
print("No APIs available for fallback, using current API") raise Exception("No APIs available for fallback")
return self._download_single(query, isrc, output_dir, quality, is_paused_callback, is_stopped_callback)
last_error = None last_error = None
for i, api in enumerate(apis, 1): for i, api in enumerate(apis, 1):