Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de798e4807 | |||
| 0e7ba6d029 | |||
| 2306b1f8d2 | |||
| 1b0d67702d |
@@ -6,7 +6,7 @@
|
|||||||
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music, and Deezer <code>(via Lucida)</code>, as well as Qobuz <code>(via SquidWTF)</code>.
|
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music, and Deezer <code>(via Lucida)</code>, as well as Qobuz <code>(via SquidWTF)</code>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v3.1/SpotiFLAC.exe)
|
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v3.3/SpotiFLAC.exe)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|||||||
+24
-32
@@ -56,7 +56,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_album_subfolders=False, use_fallback=False, service="amazon", timeout=30, qobuz_region="us"):
|
use_album_subfolders=False, service="amazon", timeout=30, qobuz_region="us"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.tracks = tracks
|
self.tracks = tracks
|
||||||
self.outpath = outpath
|
self.outpath = outpath
|
||||||
@@ -67,7 +67,6 @@ class DownloadWorker(QThread):
|
|||||||
self.filename_format = filename_format
|
self.filename_format = filename_format
|
||||||
self.use_track_numbers = use_track_numbers
|
self.use_track_numbers = use_track_numbers
|
||||||
self.use_album_subfolders = use_album_subfolders
|
self.use_album_subfolders = use_album_subfolders
|
||||||
self.use_fallback = use_fallback
|
|
||||||
self.service = service
|
self.service = service
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.qobuz_region = qobuz_region
|
self.qobuz_region = qobuz_region
|
||||||
@@ -89,7 +88,7 @@ class DownloadWorker(QThread):
|
|||||||
elif self.service == "tidal_api":
|
elif self.service == "tidal_api":
|
||||||
downloader = TidalDownloader(timeout=self.timeout)
|
downloader = TidalDownloader(timeout=self.timeout)
|
||||||
else:
|
else:
|
||||||
downloader = LucidaDownloader(self.use_fallback, self.timeout)
|
downloader = LucidaDownloader(timeout=self.timeout)
|
||||||
|
|
||||||
def progress_update(current, total):
|
def progress_update(current, total):
|
||||||
if total > 0:
|
if total > 0:
|
||||||
@@ -194,18 +193,28 @@ class DownloadWorker(QThread):
|
|||||||
return
|
return
|
||||||
elif isinstance(download_result_details, dict) and download_result_details.get("success") == False:
|
elif isinstance(download_result_details, dict) and download_result_details.get("success") == False:
|
||||||
raise Exception(download_result_details.get("error", "Tidal API download failed"))
|
raise Exception(download_result_details.get("error", "Tidal API download failed"))
|
||||||
elif isinstance(download_result_details, dict) and download_result_details.get("status") == "all_skipped" or download_result_details.get("status") == "skipped_exists":
|
elif isinstance(download_result_details, dict) and (download_result_details.get("status") == "all_skipped" or download_result_details.get("status") == "skipped_exists"):
|
||||||
self.progress.emit(f"File already exists or skipped: {new_filename}",0)
|
self.progress.emit(f"File already exists or skipped: {new_filename}",0)
|
||||||
downloaded_file = new_filepath
|
downloaded_file = new_filepath
|
||||||
else:
|
else:
|
||||||
downloaded_file = None
|
downloaded_file = None
|
||||||
raise Exception(f"Tidal API download failed or returned unexpected result: {download_result_details}")
|
raise Exception(f"Tidal API download failed or returned unexpected result: {download_result_details}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
track_id = track.id
|
track_id = track.id
|
||||||
self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0)
|
self.progress.emit(f"Getting track info for ID: {track_id} from {self.service}", 0)
|
||||||
|
|
||||||
metadata = downloader.get_track_info(track_id, self.service)
|
import asyncio
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
metadata = loop.run_until_complete(downloader.get_track_info(track_id, self.service))
|
||||||
self.progress.emit(f"Track info received, starting download process", 0)
|
self.progress.emit(f"Track info received, starting download process", 0)
|
||||||
|
|
||||||
is_paused_callback = lambda: self.is_paused
|
is_paused_callback = lambda: self.is_paused
|
||||||
@@ -307,7 +316,7 @@ class ServiceStatusChecker(QThread):
|
|||||||
services_status = {}
|
services_status = {}
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
current_services = data.get('all', {}).get('downloads', {}).get('current', {}).get('services', {})
|
current_services = data.get('downloads', {}).get('current', {}).get('services', {})
|
||||||
services_status['amazon'] = current_services.get('amazon', 0) > 0
|
services_status['amazon'] = current_services.get('amazon', 0) > 0
|
||||||
services_status['tidal'] = current_services.get('tidal', 0) > 0
|
services_status['tidal'] = current_services.get('tidal', 0) > 0
|
||||||
services_status['deezer'] = current_services.get('deezer', 0) > 0
|
services_status['deezer'] = current_services.get('deezer', 0) > 0
|
||||||
@@ -324,8 +333,8 @@ class TidalStatusChecker(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
response = requests.get("https://tidal.401658.xyz", timeout=5)
|
response = requests.get("https://hifi.401658.xyz", timeout=5)
|
||||||
is_online = response.status_code == 200
|
is_online = response.status_code == 200 or response.status_code == 429
|
||||||
self.status_updated.emit(is_online)
|
self.status_updated.emit(is_online)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error.emit(f"Error checking Tidal (API) status: {str(e)}")
|
self.error.emit(f"Error checking Tidal (API) status: {str(e)}")
|
||||||
@@ -398,10 +407,10 @@ class ServiceComboBox(QComboBox):
|
|||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
self.services = [
|
self.services = [
|
||||||
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False},
|
{'id': 'tidal', 'name': 'Tidal (Lucida)', 'icon': 'tidal.png', 'online': False},
|
||||||
{'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png', 'online': False},
|
{'id': 'amazon', 'name': 'Amazon (Lucida)', 'icon': 'amazon.png', 'online': False},
|
||||||
{'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False},
|
{'id': 'deezer', 'name': 'Deezer (Lucida)', 'icon': 'deezer.png', 'online': False},
|
||||||
{'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png', 'online': False},
|
{'id': 'qobuz', 'name': 'Qobuz (SquidWTF)', 'icon': 'qobuz.png', 'online': False},
|
||||||
{'id': 'tidal_api', 'name': 'Tidal (API)', 'icon': 'tidal.png', 'online': False}
|
{'id': 'tidal_api', 'name': 'Tidal (API)', 'icon': 'tidal.png', 'online': False}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -550,7 +559,7 @@ class QobuzRegionComboBox(QComboBox):
|
|||||||
class SpotiFLACGUI(QWidget):
|
class SpotiFLACGUI(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_version = "3.2"
|
self.current_version = "3.4"
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
|
|
||||||
@@ -561,7 +570,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.filename_format = self.settings.value('filename_format', 'title_artist')
|
self.filename_format = self.settings.value('filename_format', 'title_artist')
|
||||||
self.use_track_numbers = self.settings.value('use_track_numbers', False, type=bool)
|
self.use_track_numbers = self.settings.value('use_track_numbers', 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.use_fallback = self.settings.value('use_fallback', False, type=bool)
|
|
||||||
self.service = self.settings.value('service', 'amazon')
|
self.service = self.settings.value('service', 'amazon')
|
||||||
self.qobuz_region = self.settings.value('qobuz_region', 'us')
|
self.qobuz_region = self.settings.value('qobuz_region', 'us')
|
||||||
self.timeout_value = self.settings.value('timeout_value', 30, type=int)
|
self.timeout_value = self.settings.value('timeout_value', 30, type=int)
|
||||||
@@ -900,12 +908,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
|
|
||||||
service_fallback_layout.addSpacing(10)
|
service_fallback_layout.addSpacing(10)
|
||||||
|
|
||||||
self.fallback_checkbox = QCheckBox('Fallback')
|
|
||||||
self.fallback_checkbox.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
self.fallback_checkbox.setChecked(self.use_fallback)
|
|
||||||
self.fallback_checkbox.toggled.connect(self.save_fallback_setting)
|
|
||||||
service_fallback_layout.addWidget(self.fallback_checkbox)
|
|
||||||
|
|
||||||
timeout_label = QLabel('Timeout:')
|
timeout_label = QLabel('Timeout:')
|
||||||
self.timeout_input = QLineEdit()
|
self.timeout_input = QLineEdit()
|
||||||
self.timeout_input.setText(str(self.timeout_value))
|
self.timeout_input.setText(str(self.timeout_value))
|
||||||
@@ -995,7 +997,7 @@ class SpotiFLACGUI(QWidget):
|
|||||||
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
spacer = QSpacerItem(20, 6, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||||
about_layout.addItem(spacer)
|
about_layout.addItem(spacer)
|
||||||
|
|
||||||
footer_label = QLabel("v3.2 | June 2025")
|
footer_label = QLabel("v3.4 | June 2025")
|
||||||
footer_label.setStyleSheet("font-size: 12px; margin-top: 10px;")
|
footer_label.setStyleSheet("font-size: 12px; margin-top: 10px;")
|
||||||
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
about_layout.addWidget(footer_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
@@ -1021,7 +1023,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if service == "qobuz":
|
if service == "qobuz":
|
||||||
self.fallback_checkbox.hide()
|
|
||||||
self.timeout_input.hide()
|
self.timeout_input.hide()
|
||||||
if timeout_label:
|
if timeout_label:
|
||||||
timeout_label.hide()
|
timeout_label.hide()
|
||||||
@@ -1030,7 +1031,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
region_label.show()
|
region_label.show()
|
||||||
self.qobuz_region_dropdown.show()
|
self.qobuz_region_dropdown.show()
|
||||||
elif service == "tidal_api":
|
elif service == "tidal_api":
|
||||||
self.fallback_checkbox.hide()
|
|
||||||
self.timeout_input.hide()
|
self.timeout_input.hide()
|
||||||
if timeout_label:
|
if timeout_label:
|
||||||
timeout_label.hide()
|
timeout_label.hide()
|
||||||
@@ -1038,7 +1038,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
region_label.hide()
|
region_label.hide()
|
||||||
self.qobuz_region_dropdown.hide()
|
self.qobuz_region_dropdown.hide()
|
||||||
else:
|
else:
|
||||||
self.fallback_checkbox.show()
|
|
||||||
self.timeout_input.show()
|
self.timeout_input.show()
|
||||||
if timeout_label:
|
if timeout_label:
|
||||||
timeout_label.show()
|
timeout_label.show()
|
||||||
@@ -1068,12 +1067,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.settings.setValue('use_album_subfolders', self.use_album_subfolders)
|
self.settings.setValue('use_album_subfolders', self.use_album_subfolders)
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
|
|
||||||
def save_fallback_setting(self):
|
|
||||||
self.use_fallback = self.fallback_checkbox.isChecked()
|
|
||||||
self.settings.setValue('use_fallback', self.use_fallback)
|
|
||||||
self.settings.sync()
|
|
||||||
self.log_output.append("Fallback setting saved successfully!")
|
|
||||||
|
|
||||||
def save_timeout_setting(self):
|
def save_timeout_setting(self):
|
||||||
try:
|
try:
|
||||||
timeout = int(self.timeout_input.text())
|
timeout = int(self.timeout_input.text())
|
||||||
@@ -1388,7 +1381,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.filename_format,
|
self.filename_format,
|
||||||
self.use_track_numbers,
|
self.use_track_numbers,
|
||||||
self.use_album_subfolders,
|
self.use_album_subfolders,
|
||||||
self.use_fallback,
|
|
||||||
service,
|
service,
|
||||||
self.timeout_value,
|
self.timeout_value,
|
||||||
qobuz_region
|
qobuz_region
|
||||||
|
|||||||
+20
-8
@@ -21,7 +21,7 @@ class ProgressCallback:
|
|||||||
print(f"\r{current / (1024 * 1024):.2f} MB", end="")
|
print(f"\r{current / (1024 * 1024):.2f} MB", end="")
|
||||||
|
|
||||||
class LucidaDownloader:
|
class LucidaDownloader:
|
||||||
def __init__(self, domain="to", timeout=30):
|
def __init__(self, timeout=30):
|
||||||
self.client = requests.Session()
|
self.client = requests.Session()
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
@@ -29,10 +29,7 @@ class LucidaDownloader:
|
|||||||
self.progress_callback = ProgressCallback()
|
self.progress_callback = ProgressCallback()
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
if domain not in ["to", "su"]:
|
self.base_domain = "lucida.to"
|
||||||
raise ValueError("Domain must be either 'to' or 'su'")
|
|
||||||
|
|
||||||
self.base_domain = f"lucida.{domain}"
|
|
||||||
|
|
||||||
def set_progress_callback(self, callback):
|
def set_progress_callback(self, callback):
|
||||||
self.progress_callback = callback
|
self.progress_callback = callback
|
||||||
@@ -674,7 +671,7 @@ class TidalDownloader:
|
|||||||
|
|
||||||
async def get_track_download_info(self, track_id, quality="LOSSLESS"):
|
async def get_track_download_info(self, track_id, quality="LOSSLESS"):
|
||||||
try:
|
try:
|
||||||
download_api_url = f"https://tidal.401658.xyz/track/?id={track_id}&quality={quality}"
|
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:
|
async with httpx.AsyncClient(http2=True, timeout=self.timeout) as client:
|
||||||
response = await client.get(download_api_url)
|
response = await client.get(download_api_url)
|
||||||
@@ -975,7 +972,15 @@ class TidalDownloader:
|
|||||||
print(f"Search error: {search_result['error']}")
|
print(f"Search error: {search_result['error']}")
|
||||||
return {"success": False, "error": search_result['error']}
|
return {"success": False, "error": search_result['error']}
|
||||||
|
|
||||||
|
raw_result = None
|
||||||
|
if isrc:
|
||||||
|
raw_result = await self.search_tracks(query)
|
||||||
|
|
||||||
if not search_result["items"]:
|
if not search_result["items"]:
|
||||||
|
if isrc and raw_result and raw_result.get("items"):
|
||||||
|
print("No tracks found with ISRC filter, falling back to unfiltered search")
|
||||||
|
search_result = raw_result
|
||||||
|
else:
|
||||||
print("No tracks found")
|
print("No tracks found")
|
||||||
return {"success": False, "error": "No tracks found"}
|
return {"success": False, "error": "No tracks found"}
|
||||||
|
|
||||||
@@ -1007,9 +1012,16 @@ class TidalDownloader:
|
|||||||
raise Exception("Download stopped by user")
|
raise Exception("Download stopped by user")
|
||||||
raise Exception(result["error"])
|
raise Exception(result["error"])
|
||||||
|
|
||||||
|
def print_progress(current, total):
|
||||||
|
if total > 0:
|
||||||
|
percent = (current / total) * 100
|
||||||
|
print(f"\rProgress: {percent:.2f}% ({current}/{total})", end="")
|
||||||
|
else:
|
||||||
|
print(f"\rDownloaded: {current / (1024 * 1024):.2f} MB", end="")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
print("=== LucidaDownloader ===")
|
print("=== LucidaDownloader ===")
|
||||||
lucida = LucidaDownloader(domain="to")
|
lucida = LucidaDownloader()
|
||||||
track_id = "2plbrEY59IikOBgBGLjaoe"
|
track_id = "2plbrEY59IikOBgBGLjaoe"
|
||||||
service = "tidal"
|
service = "tidal"
|
||||||
output_dir = "."
|
output_dir = "."
|
||||||
@@ -1018,7 +1030,7 @@ async def main():
|
|||||||
print(f"Getting track: {track_id} from {service}")
|
print(f"Getting track: {track_id} from {service}")
|
||||||
metadata = await lucida.get_track_info(track_id, service)
|
metadata = await lucida.get_track_info(track_id, service)
|
||||||
print("Starting download")
|
print("Starting download")
|
||||||
downloaded_file = await lucida.download(metadata, output_dir)
|
downloaded_file = lucida.download(metadata, output_dir)
|
||||||
print(f"Success: File saved as {downloaded_file}")
|
print(f"Success: File saved as {downloaded_file}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {str(e)}")
|
print(f"Error: {str(e)}")
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "3.1"
|
"version": "3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user