Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 711c5a98d3 | |||
| fd949a17f0 | |||
| 1c0de8b3ac | |||
| b653a8ca41 | |||
| 2d0c174c50 | |||
| c63eeccc55 | |||
| a620c16b1c | |||
| cf27ae098d | |||
| a0c60a473a |
@@ -6,7 +6,7 @@
|
|||||||
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal & Deezer.
|
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal & Deezer.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v5.1/SpotiFLAC.exe)
|
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/latest/download/SpotiFLAC.exe)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Lossless Audio Check
|
## Lossless Audio Check
|
||||||
|
|
||||||
|
|||||||
+168
-42
@@ -125,11 +125,14 @@ class DownloadWorker(QThread):
|
|||||||
def run(self):
|
def run(self):
|
||||||
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:
|
||||||
@@ -157,10 +160,12 @@ class DownloadWorker(QThread):
|
|||||||
if self.use_artist_subfolders:
|
if self.use_artist_subfolders:
|
||||||
artist_name = track.artists.split(', ')[0] if ', ' in track.artists else track.artists
|
artist_name = track.artists.split(', ')[0] if ', ' in track.artists else track.artists
|
||||||
artist_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', artist_name)
|
artist_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', artist_name)
|
||||||
|
artist_folder = artist_folder.rstrip('. ')
|
||||||
track_outpath = os.path.join(track_outpath, artist_folder)
|
track_outpath = os.path.join(track_outpath, artist_folder)
|
||||||
|
|
||||||
if self.use_album_subfolders:
|
if self.use_album_subfolders:
|
||||||
album_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', track.album)
|
album_folder = re.sub(r'[<>:"/\\|?*]', lambda m: "'" if m.group() == '"' else '_', track.album)
|
||||||
|
album_folder = album_folder.rstrip('. ')
|
||||||
track_outpath = os.path.join(track_outpath, album_folder)
|
track_outpath = os.path.join(track_outpath, album_folder)
|
||||||
|
|
||||||
os.makedirs(track_outpath, exist_ok=True)
|
os.makedirs(track_outpath, exist_ok=True)
|
||||||
@@ -314,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))
|
||||||
@@ -416,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)
|
||||||
@@ -438,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)
|
||||||
@@ -445,23 +551,28 @@ 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_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()
|
|
||||||
|
|
||||||
self.deezer_status_timer = QTimer(self)
|
self.deezer_status_timer = QTimer(self)
|
||||||
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status)
|
self.deezer_status_timer.timeout.connect(self.refresh_deezer_status)
|
||||||
self.deezer_status_timer.start(60000)
|
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.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 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__))
|
||||||
@@ -535,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.2"
|
self.current_version = "5.4"
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.all_tracks = []
|
self.all_tracks = []
|
||||||
self.successful_downloads = []
|
self.successful_downloads = []
|
||||||
@@ -550,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')
|
||||||
@@ -1139,24 +1250,26 @@ class SpotiFLACGUI(QWidget):
|
|||||||
auth_layout.addWidget(auth_label)
|
auth_layout.addWidget(auth_label)
|
||||||
|
|
||||||
service_api_layout = QHBoxLayout()
|
service_api_layout = QHBoxLayout()
|
||||||
|
|
||||||
service_label = QLabel('Service:')
|
service_label = QLabel('Service:')
|
||||||
service_label.setFixedWidth(60)
|
service_label.setFixedWidth(53)
|
||||||
|
|
||||||
self.service_dropdown = ServiceComboBox()
|
self.service_dropdown = ServiceComboBox()
|
||||||
self.service_dropdown.setFixedWidth(120)
|
self.service_dropdown.setFixedWidth(100)
|
||||||
self.service_dropdown.currentIndexChanged.connect(self.on_service_changed)
|
self.service_dropdown.currentIndexChanged.connect(self.on_service_changed)
|
||||||
|
|
||||||
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.tidal_api_label = QLabel('Tidal API:')
|
self.api_spacer = QWidget()
|
||||||
self.tidal_api_label.setFixedWidth(70)
|
self.api_spacer.setFixedWidth(15)
|
||||||
|
service_api_layout.addWidget(self.api_spacer)
|
||||||
|
|
||||||
|
self.tidal_api_label = QLabel('API Instances:')
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -1166,10 +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)
|
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()
|
service_api_layout.addStretch(1)
|
||||||
|
|
||||||
auth_layout.addLayout(service_api_layout)
|
auth_layout.addLayout(service_api_layout)
|
||||||
|
|
||||||
@@ -1407,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)
|
||||||
@@ -1423,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)
|
||||||
@@ -1433,41 +1547,51 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.tidal_api = selected_api
|
self.tidal_api = selected_api
|
||||||
self.settings.setValue('tidal_api', selected_api)
|
self.settings.setValue('tidal_api', selected_api)
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.log_output.append(f"Tidal API changed to: {self.tidal_api_dropdown.currentText()}")
|
self.log_output.append(f"API Instance changed to: {self.tidal_api_dropdown.currentText()}")
|
||||||
|
|
||||||
def refresh_tidal_apis(self):
|
def refresh_tidal_apis(self):
|
||||||
try:
|
try:
|
||||||
self.log_output.append("Fetching available Tidal APIs...")
|
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 Tidal APIs")
|
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())
|
||||||
@@ -1914,6 +2038,7 @@ class SpotiFLACGUI(QWidget):
|
|||||||
if self.is_album or self.is_playlist:
|
if self.is_album or self.is_playlist:
|
||||||
name = self.album_or_playlist_name.strip()
|
name = self.album_or_playlist_name.strip()
|
||||||
folder_name = re.sub(r'[<>:"/\\|?*]', '_', name)
|
folder_name = re.sub(r'[<>:"/\\|?*]', '_', name)
|
||||||
|
folder_name = folder_name.rstrip('. ')
|
||||||
outpath = os.path.join(outpath, folder_name)
|
outpath = os.path.join(outpath, folder_name)
|
||||||
os.makedirs(outpath, exist_ok=True)
|
os.makedirs(outpath, exist_ok=True)
|
||||||
|
|
||||||
@@ -1965,6 +2090,7 @@ class SpotiFLACGUI(QWidget):
|
|||||||
|
|
||||||
self.stop_btn.show()
|
self.stop_btn.show()
|
||||||
self.pause_resume_btn.show()
|
self.pause_resume_btn.show()
|
||||||
|
self.remove_successful_btn.hide()
|
||||||
self.progress_bar.show()
|
self.progress_bar.show()
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
"vogel.qqdl.site",
|
||||||
|
"maus.qqdl.site",
|
||||||
|
"hund.qqdl.site",
|
||||||
|
"eu-maus.qqdl.site",
|
||||||
|
"eu-katze.qqdl.site",
|
||||||
|
"katze.qqdl.site",
|
||||||
|
"wolf.qqdl.site",
|
||||||
|
"zeus.squid.wtf",
|
||||||
|
"shiva.squid.wtf",
|
||||||
|
"kraken.squid.wtf",
|
||||||
|
"dev-api.squid.wtf",
|
||||||
|
"chaos.squid.wtf",
|
||||||
|
"phoenix.squid.wtf",
|
||||||
|
"triton.squid.wtf",
|
||||||
|
"aether.squid.wtf"
|
||||||
|
]
|
||||||
+27
-37
@@ -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: '):
|
api_instances = [{"url": f"https://{api}"} for api in api_list]
|
||||||
data = json.loads(line_str[6:])
|
|
||||||
|
return api_instances
|
||||||
api_instances = [
|
else:
|
||||||
inst for inst in data.get('instances', [])
|
print(f"Failed to fetch API list: HTTP {response.status_code}")
|
||||||
if inst.get('instance_type') == 'api' and inst.get('last_check', {}).get('success')
|
return []
|
||||||
]
|
|
||||||
|
|
||||||
api_instances.sort(key=lambda x: x.get('avg_response_time', 9999))
|
|
||||||
|
|
||||||
return api_instances
|
|
||||||
|
|
||||||
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 Tidal APIs ===")
|
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("\n0 Use default (https://hifi.401658.xyz)")
|
print("-" * 60)
|
||||||
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
|
||||||
@@ -253,6 +240,10 @@ class TidalDownloader:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
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):
|
||||||
|
file_dir = os.path.dirname(filepath)
|
||||||
|
if file_dir and not os.path.exists(file_dir):
|
||||||
|
os.makedirs(file_dir, exist_ok=True)
|
||||||
|
|
||||||
temp_filepath = filepath + ".part"
|
temp_filepath = filepath + ".part"
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
|
|
||||||
@@ -395,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):
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "5.1"
|
"version": "5.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user