Compare commits

...

7 Commits

Author SHA1 Message Date
afkarxyz d6abe2bae3 Update v2.0 2025-03-02 14:48:15 +07:00
afkarxyz 7b858dd0ce Update v2.0 2025-03-02 14:47:55 +07:00
afkarxyz cfeb9a2ef2 Update v1.9 2025-03-02 14:05:17 +07:00
afkarxyz 81a78832ff Update v1.9 2025-03-02 14:02:16 +07:00
afkarxyz 071f20deff Update README.md 2025-03-02 14:01:46 +07:00
afkarxyz 03de68ac7b Rename to getMetadata.py 2025-03-02 06:45:20 +07:00
afkarxyz 77363f9e61 Update README.md 2025-03-02 06:33:53 +07:00
5 changed files with 170 additions and 32 deletions
+5 -5
View File
@@ -3,10 +3,10 @@
![spotiflac](https://github.com/user-attachments/assets/a233a276-14a4-4f4c-b267-f182dd3912a0) ![spotiflac](https://github.com/user-attachments/assets/a233a276-14a4-4f4c-b267-f182dd3912a0)
<div align="center"> <div align="center">
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Qobuz with the help of Lucida. <b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Deezer with the help of Lucida.
</div> </div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v1.8/SpotiFLAC.exe) ### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v1.9/SpotiFLAC.exe)
# #
@@ -15,9 +15,9 @@ Sometimes, the **download speed** from Lucida can be fast or slow; it varies unp
## Screenshots ## Screenshots
> When **Fallback Server** is enabled, it will use the backup server Lucida.su > When **Fallback** is enabled, it will use the backup server `Lucida.su`
![image](https://github.com/user-attachments/assets/d28c2803-d9b4-4150-bd20-dd98df348e64) ![image](https://github.com/user-attachments/assets/3db51367-45dc-470f-8d6e-8f783ebd6340)
![image](https://github.com/user-attachments/assets/a9020973-f79c-40ba-ab76-e4a3955a1ba4) ![image](https://github.com/user-attachments/assets/a9020973-f79c-40ba-ab76-e4a3955a1ba4)
@@ -29,4 +29,4 @@ Sometimes, the **download speed** from Lucida can be fast or slow; it varies unp
![image](https://github.com/user-attachments/assets/7649e6e1-d5d1-49b3-b83f-965d44651d05) ![image](https://github.com/user-attachments/assets/7649e6e1-d5d1-49b3-b83f-965d44651d05)
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v0/FLAC-Checker.zip) FLAC Checker #### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v0/FLAC-Checker.zip) FLAC Checker
+112 -22
View File
@@ -1,16 +1,17 @@
import sys import sys
import os import os
import requests
import time import time
from datetime import datetime from datetime import datetime
import requests
from pathlib import Path from pathlib import Path
from packaging import version from packaging import version
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QProgressBar, QFileDialog, QCheckBox, QRadioButton, QProgressBar, QFileDialog, QCheckBox, QRadioButton,
QGroupBox, QComboBox, QDialog, QDialogButtonBox) QGroupBox, QComboBox, QDialog, QDialogButtonBox,
QStyledItemDelegate, QStyle)
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QSettings, QSize, QTimer, QUrl from PyQt6.QtCore import QThread, pyqtSignal, Qt, QSettings, QSize, QTimer, QUrl
from PyQt6.QtGui import QIcon, QPixmap, QCursor,QDesktopServices from PyQt6.QtGui import QIcon, QPixmap, QCursor, QDesktopServices, QBrush, QPalette
from getTracks import TrackDownloader from getTracks import TrackDownloader
class ImageDownloader(QThread): class ImageDownloader(QThread):
@@ -129,7 +130,10 @@ class DownloaderWorker(QThread):
time_diff = current_time - self.last_update_time time_diff = current_time - self.last_update_time
if time_diff > 0: if time_diff > 0:
speed = (downloaded_size - self.last_downloaded_size) / time_diff speed = (downloaded_size - self.last_downloaded_size) / time_diff
status = f"Downloading... {self.format_size(downloaded_size)}/{self.format_size(total_size)} | {self.format_speed(speed)}" if downloaded_size == 0 and total_size == 0:
status = "Preparing metadata..."
else:
status = f"Downloading... {self.format_size(downloaded_size)}/{self.format_size(total_size)} | {self.format_speed(speed)}"
self.status.emit(status) self.status.emit(status)
self.last_update_time = current_time self.last_update_time = current_time
@@ -147,36 +151,122 @@ class DownloaderWorker(QThread):
except Exception as e: except Exception as e:
self.error.emit(f"Error: {str(e)}") self.error.emit(f"Error: {str(e)}")
class ServiceStatusChecker(QThread):
status_updated = pyqtSignal(dict)
error = pyqtSignal(str)
def run(self):
try:
response = requests.get("https://lucida.to/api/stats", timeout=5)
if response.status_code == 200:
data = response.json()
services_status = {}
current_services = data.get('all', {}).get('downloads', {}).get('current', {}).get('services', {})
services_status['amazon'] = current_services.get('amazon', 0) > 0
services_status['tidal'] = current_services.get('tidal', 0) > 0
services_status['deezer'] = current_services.get('deezer', 0) > 0
self.status_updated.emit(services_status)
else:
self.error.emit(f"Server returned status code: {response.status_code}")
except Exception as e:
self.error.emit(f"Error checking service status: {str(e)}")
class StatusIndicatorDelegate(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
super().paint(painter, option, index)
if option.state & QStyle.StateFlag.State_Selected:
text_color = option.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText)
else:
text_color = option.palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.Text)
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 - 10
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 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(StatusIndicatorDelegate())
self.setup_items() self.setup_items()
self.status_checker = ServiceStatusChecker()
self.status_checker.status_updated.connect(self.update_service_status)
self.status_checker.error.connect(lambda e: print(f"Status check error: {e}"))
self.status_checker.start()
self.status_timer = QTimer(self)
self.status_timer.timeout.connect(self.refresh_status)
self.status_timer.start(5000)
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__))
icons_dir = os.path.join(current_dir, 'icons')
if not os.path.exists(icons_dir): self.services = [
os.makedirs(icons_dir) {'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png', 'online': False},
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png', 'online': False},
services = [ {'id': 'deezer', 'name': 'Deezer', 'icon': 'deezer.png', 'online': False}
{'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png'},
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png'}
] ]
for service in services: for service in self.services:
icon_path = os.path.join(icons_dir, service['icon']) icon_path = os.path.join(current_dir, service['icon'])
if not os.path.exists(icon_path): if not os.path.exists(icon_path):
self.create_placeholder_icon(icon_path) self.create_placeholder_icon(icon_path)
icon = QIcon(icon_path) icon = QIcon(icon_path)
self.addItem(icon, service['name'], service['id'])
self.addItem(icon, service['name'])
item_index = self.count() - 1
self.setItemData(item_index, service['id'], Qt.ItemDataRole.UserRole + 1)
self.setItemData(item_index, service, Qt.ItemDataRole.UserRole)
def create_placeholder_icon(self, path): def create_placeholder_icon(self, path):
pixmap = QPixmap(16, 16) pixmap = QPixmap(16, 16)
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)
pixmap.save(path) pixmap.save(path)
def update_service_status(self, status_dict):
self.services_status = status_dict
for i in range(self.count()):
service_id = self.itemData(i, Qt.ItemDataRole.UserRole + 1)
if service_id in self.services_status:
service_data = self.itemData(i, Qt.ItemDataRole.UserRole)
if isinstance(service_data, dict):
service_data['online'] = self.services_status[service_id]
self.setItemData(i, service_data, Qt.ItemDataRole.UserRole)
self.update()
def refresh_status(self):
self.status_checker = ServiceStatusChecker()
self.status_checker.status_updated.connect(self.update_service_status)
self.status_checker.error.connect(lambda e: print(f"Status check error: {e}"))
self.status_checker.start()
def currentData(self, role=Qt.ItemDataRole.UserRole + 1):
return super().currentData(role)
class UpdateDialog(QDialog): class UpdateDialog(QDialog):
def __init__(self, current_version, new_version, parent=None): def __init__(self, current_version, new_version, parent=None):
@@ -216,7 +306,7 @@ class UpdateDialog(QDialog):
class SpotiFlacGUI(QMainWindow): class SpotiFlacGUI(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "1.8" self.current_version = "2.0"
self.settings = QSettings('SpotiFlac', 'Settings') self.settings = QSettings('SpotiFlac', 'Settings')
self.setWindowTitle("SpotiFLAC") self.setWindowTitle("SpotiFLAC")
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)
@@ -275,7 +365,7 @@ class SpotiFlacGUI(QMainWindow):
self.fallback_checkbox.setChecked(fallback) self.fallback_checkbox.setChecked(fallback)
for i in range(self.service_combo.count()): for i in range(self.service_combo.count()):
if self.service_combo.itemData(i) == service: if self.service_combo.itemData(i, Qt.ItemDataRole.UserRole + 1) == service:
self.service_combo.setCurrentIndex(i) self.service_combo.setCurrentIndex(i)
break break
@@ -287,7 +377,7 @@ class SpotiFlacGUI(QMainWindow):
self.fallback_checkbox.stateChanged.connect( self.fallback_checkbox.stateChanged.connect(
lambda x: self.settings.setValue('fallback', bool(x))) lambda x: self.settings.setValue('fallback', bool(x)))
self.service_combo.currentIndexChanged.connect( self.service_combo.currentIndexChanged.connect(
lambda i: self.settings.setValue('service', self.service_combo.itemData(i))) lambda i: self.settings.setValue('service', self.service_combo.itemData(i, Qt.ItemDataRole.UserRole + 1)))
self.format_title_artist.toggled.connect( self.format_title_artist.toggled.connect(
lambda x: self.settings.setValue('format', 'title_artist' if x else 'artist_title')) lambda x: self.settings.setValue('format', 'title_artist' if x else 'artist_title'))
self.dir_input.textChanged.connect( self.dir_input.textChanged.connect(
@@ -342,7 +432,7 @@ class SpotiFlacGUI(QMainWindow):
settings_container_layout.setContentsMargins(0, 0, 0, 0) settings_container_layout.setContentsMargins(0, 0, 0, 0)
settings_container_layout.setSpacing(10) settings_container_layout.setSpacing(10)
self.fallback_checkbox = QCheckBox("Fallback Server") self.fallback_checkbox = QCheckBox("Fallback")
self.fallback_checkbox.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.fallback_checkbox.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.fallback_checkbox.setChecked(False) self.fallback_checkbox.setChecked(False)
settings_container_layout.addWidget(self.fallback_checkbox) settings_container_layout.addWidget(self.fallback_checkbox)
@@ -366,9 +456,9 @@ class SpotiFlacGUI(QMainWindow):
format_layout.setContentsMargins(0, 0, 0, 0) format_layout.setContentsMargins(0, 0, 0, 0)
format_layout.setSpacing(10) format_layout.setSpacing(10)
format_label = QLabel("Filename Format:") format_label = QLabel("Filename:")
self.format_title_artist = QRadioButton("Title") self.format_title_artist = QRadioButton("Title - Artist")
self.format_artist_title = QRadioButton("Artist") self.format_artist_title = QRadioButton("Artist - Title")
self.format_title_artist.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.format_title_artist.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.format_artist_title.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.format_artist_title.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.format_title_artist.setChecked(True) self.format_title_artist.setChecked(True)
@@ -683,4 +773,4 @@ def main():
sys.exit(app.exec()) sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+52 -4
View File
@@ -101,10 +101,31 @@ class TrackDownloader:
print("Waiting for track processing to complete") print("Waiting for track processing to complete")
while True: while True:
completion_response = self.client.get(completion_url, headers=self.headers).json() completion_response = self.client.get(completion_url, headers=self.headers).json()
if completion_response["status"] == "completed":
status = completion_response["status"]
if status == "completed":
print("Processing completed: 100%")
break break
elif completion_response["status"] == "error": elif status == "error":
raise Exception(f"API request failed: {completion_response.get('message', 'Unknown error')}") raise Exception(f"API request failed: {completion_response.get('message', 'Unknown error')}")
else:
progress = completion_response.get("progress", {})
if progress:
current = progress.get("current", 0)
total = progress.get("total", 100)
percent = int((current / total) * 100) if total > 0 else 0
action = progress.get("action", "Processing")
print(f"Progress: {percent}% - {action} ({current}/{total})")
if action.lower() == "metadata":
if self.progress_callback:
self.progress_callback(0, 0)
else:
print(f"Status: {status} - Waiting for progress information...")
if status.lower() == "metadata":
if self.progress_callback:
self.progress_callback(0, 0)
time.sleep(1) time.sleep(1)
download_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}/download" download_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}/download"
@@ -118,16 +139,33 @@ class TrackDownloader:
try: try:
with open(file_path, 'wb') as file: with open(file_path, 'wb') as file:
start_time = time.time()
last_update_time = start_time
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
if chunk: if chunk:
file.write(chunk) file.write(chunk)
downloaded_size += len(chunk) downloaded_size += len(chunk)
current_time = time.time()
if current_time - last_update_time >= 1:
if total_size > 0:
progress_percent = (downloaded_size / total_size) * 100
elapsed_time = current_time - start_time
speed = downloaded_size / (1024 * 1024 * elapsed_time) if elapsed_time > 0 else 0
print(f"Download progress: {progress_percent:.2f}% ({downloaded_size}/{total_size}) - {speed:.2f} MB/s")
else:
print(f"Downloaded {downloaded_size / (1024 * 1024):.2f} MB")
last_update_time = current_time
if self.progress_callback: if self.progress_callback:
self.progress_callback(downloaded_size, total_size) self.progress_callback(downloaded_size, total_size)
if downloaded_size == 0: if downloaded_size == 0:
raise Exception("No data received from server") raise Exception("No data received from server")
print(f"Download completed: {file_path}")
return file_path return file_path
except Exception as e: except Exception as e:
@@ -144,12 +182,22 @@ async def main():
track_id = "2plbrEY59IikOBgBGLjaoe" track_id = "2plbrEY59IikOBgBGLjaoe"
service = "amazon" service = "amazon"
def progress_update(current, total):
if total > 0:
percent = (current / total) * 100
print(f"\rDownload progress: {percent:.2f}% ({current}/{total})", end="")
downloader.set_progress_callback(progress_update)
try: try:
print(f"Getting track info for ID: {track_id} from {service}")
metadata = await downloader.get_track_info(track_id, service) metadata = await downloader.get_track_info(track_id, service)
print(f"Track info received: {metadata['title']} by {metadata['artists']}")
downloaded_file = downloader.download(metadata, output_dir) downloaded_file = downloader.download(metadata, output_dir)
print(f"File downloaded successfully: {downloaded_file}") print(f"\nFile downloaded successfully: {downloaded_file}")
except Exception as e: except Exception as e:
print(f"An error occurred: {str(e)}") print(f"An error occurred: {str(e)}")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "1.8" "version": "1.9"
} }