Compare commits

...

7 Commits

Author SHA1 Message Date
afkarxyz 61c53655ff Update v1.2 2025-01-09 23:16:16 +07:00
afkarxyz 77bbc70c9b Update README.md 2025-01-09 23:15:32 +07:00
afkarxyz 360ba44dd5 Update GetMetadata.py 2025-01-09 19:53:50 +07:00
afkarxyz 9011335181 Update LucidaDownloader.py 2025-01-09 19:53:38 +07:00
afkarxyz 3d0f21bf57 Update v1.1 2025-01-09 19:53:26 +07:00
afkarxyz b6fa1ec6a5 Update README.md 2025-01-09 19:49:38 +07:00
afkarxyz 9bfe08cad0 Update README.md 2025-01-09 15:38:10 +07:00
4 changed files with 114 additions and 27 deletions
+3 -3
View File
@@ -14,9 +14,9 @@ async def get_metadata(page, headless=True):
const [url, config] = args;
if (url.includes('/api/load?url=%2Fapi%2Ffetch%2Fstream%2Fv2')) {
const payload = JSON.parse(config.body);
const title = document.querySelector('h1.svelte-6pt9ji').textContent;
const title = document.querySelector('h1.svelte-6pt9ji').textContent.trim();
const artists = Array.from(document.querySelectorAll('h2.svelte-6pt9ji a.normal'))
.map(a => a.textContent)
.map(a => a.textContent.trim())
.join(', ');
const cover = document.querySelector('.svelte-6pt9ji .meta.svelte-6pt9ji a').href;
@@ -96,4 +96,4 @@ async def main(headless=True):
await browser.stop()
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
+2 -3
View File
@@ -111,8 +111,7 @@ class TrackDownloader:
file.write(chunk)
downloaded_size += len(chunk)
if self.progress_callback:
progress = int((downloaded_size / total_size) * 100) if total_size > 0 else 0
self.progress_callback(progress)
self.progress_callback(downloaded_size, total_size)
if downloaded_size == 0:
raise Exception("No data received from server")
@@ -139,4 +138,4 @@ async def main():
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
+9 -9
View File
@@ -1,26 +1,26 @@
[![GitHub All Releases](https://img.shields.io/github/downloads/afkarxyz/SpotifyFLAC/total?style=for-the-badge)](https://github.com/afkarxyz/SpotifyFLAC/releases)
**Spotify FLAC** allows you to download Spotify tracks in true, lossless FLAC format, providing the highest audio quality for an exceptional listening experience.
**Spotify FLAC** allows you to download Spotify tracks in true FLAC format using services like Tidal, Qobuz, and Amazon Music with the help of Lucida.
> [!NOTE]
> Requires **Google Chrome**
#### [Download](https://github.com/afkarxyz/SpotifyFLAC/releases/download/v1.0/SpotifyFLAC.exe) Spotify FLAC
#### [Download](https://github.com/afkarxyz/SpotifyFLAC/releases/download/v1.2/SpotifyFLAC.exe) Spotify FLAC
## Screenshots
![image](https://github.com/user-attachments/assets/94f4cf8f-eac7-4f97-ae7e-10f47f6aeac6)
![image](https://github.com/user-attachments/assets/d984b31f-aa41-446b-b0eb-7e303610f153)
![image](https://github.com/user-attachments/assets/2bad18c8-ed59-4533-b5c5-6323e6e27f36)
![image](https://github.com/user-attachments/assets/abcb01f3-ff3e-4496-afec-df720553a189)
> When **Headless** is enabled, the browser runs in the background without a graphical interface, improving performance and allowing seamless automation.
![image](https://github.com/user-attachments/assets/75a61cef-05a8-4f2c-b40b-ba5d49885ffe)
![image](https://github.com/user-attachments/assets/84dfcfec-7c9d-4b5b-8624-3558cd3155be)
## Lossless Audio Check
![image](https://github.com/user-attachments/assets/649b85e8-d96f-4c80-a652-177b26cf621c)
![image](https://github.com/user-attachments/assets/d63b422d-0ea3-4307-850f-96c99d7eaa9a)
![image](https://github.com/user-attachments/assets/7ffd0367-83d6-4136-8a45-bb35c547a8c6)
![image](https://github.com/user-attachments/assets/7649e6e1-d5d1-49b3-b83f-965d44651d05)
#### [Download](https://github.com/afkarxyz/SpotifyFLAC/releases/download/v0/FLAC-Checker.zip) FLAC Checker
+100 -12
View File
@@ -1,12 +1,13 @@
import sys
import asyncio
import os
import time
from pathlib import Path
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QProgressBar, QFileDialog, QCheckBox, QRadioButton,
QGroupBox)
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QSettings
QGroupBox, QComboBox)
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QSettings, QSize
from PyQt6.QtGui import QIcon, QPixmap, QCursor
from GetMetadata import get_metadata
from LucidaDownloader import TrackDownloader
@@ -28,10 +29,11 @@ class MetadataFetcher(QThread):
finished = pyqtSignal(dict)
error = pyqtSignal(str)
def __init__(self, url, headless=True):
def __init__(self, url, headless=True, service="tidal"):
super().__init__()
self.url = url
self.headless_mode = headless
self.service = service
self.max_retries = 3
def extract_track_id(self, url):
@@ -45,7 +47,7 @@ class MetadataFetcher(QThread):
for attempt in range(self.max_retries):
try:
lucida_url = f"https://lucida.to/?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F{track_id}&country=auto&to=tidal"
lucida_url = f"https://lucida.to/?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F{track_id}&country=auto&to={self.service}"
browser = await zd.start(headless=self.headless_mode)
try:
page = await browser.get(lucida_url)
@@ -82,6 +84,7 @@ class MetadataFetcher(QThread):
class DownloaderWorker(QThread):
progress = pyqtSignal(int)
status = pyqtSignal(str)
finished = pyqtSignal(str)
error = pyqtSignal(str)
@@ -91,10 +94,34 @@ class DownloaderWorker(QThread):
self.output_dir = output_dir
self.filename_format = filename_format
self.downloader = TrackDownloader()
self.last_update_time = 0
self.last_downloaded_size = 0
def format_size(self, size_bytes):
return f"{size_bytes / (1024 * 1024):.2f}MB"
def format_speed(self, speed_bytes):
return f"{speed_bytes * 8 / (1024 * 1024):.2f}Mbps"
def progress_callback(self, downloaded_size, total_size):
current_time = time.time()
if current_time - self.last_update_time >= 0.5:
progress = int((downloaded_size / total_size) * 100) if total_size > 0 else 0
self.progress.emit(progress)
time_diff = current_time - self.last_update_time
if time_diff > 0:
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)}"
self.status.emit(status)
self.last_update_time = current_time
self.last_downloaded_size = downloaded_size
def run(self):
try:
self.downloader.set_progress_callback(self.progress.emit)
self.status.emit("Preparing...")
self.downloader.set_progress_callback(self.progress_callback)
self.downloader.set_filename_format(self.filename_format)
self.progress.emit(0)
downloaded_file = self.downloader.download(self.metadata, self.output_dir)
@@ -103,6 +130,38 @@ class DownloaderWorker(QThread):
except Exception as e:
self.error.emit(f"Error: {str(e)}")
class ServiceComboBox(QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setIconSize(QSize(16, 16))
self.setup_items()
def setup_items(self):
current_dir = os.path.dirname(os.path.abspath(__file__))
icons_dir = os.path.join(current_dir, 'icons')
if not os.path.exists(icons_dir):
os.makedirs(icons_dir)
services = [
{'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png'},
{'id': 'amazon', 'name': 'Amazon Music', 'icon': 'amazon.png'},
{'id': 'qobuz', 'name': 'Qobuz', 'icon': 'qobuz.png'}
]
for service in services:
icon_path = os.path.join(icons_dir, service['icon'])
if not os.path.exists(icon_path):
self.create_placeholder_icon(icon_path)
icon = QIcon(icon_path)
self.addItem(icon, service['name'], service['id'])
def create_placeholder_icon(self, path):
pixmap = QPixmap(16, 16)
pixmap.fill(Qt.GlobalColor.transparent)
pixmap.save(path)
class SpotifyFlacGUI(QMainWindow):
def __init__(self):
super().__init__()
@@ -128,9 +187,17 @@ class SpotifyFlacGUI(QMainWindow):
def load_settings(self):
headless = self.settings.value('headless', True, type=bool)
service = self.settings.value('service', 'tidal')
format_type = self.settings.value('format', 'title_artist')
output_dir = self.settings.value('output_dir', self.default_music_dir)
self.headless_checkbox.setChecked(headless)
for i in range(self.service_combo.count()):
if self.service_combo.itemData(i) == service:
self.service_combo.setCurrentIndex(i)
break
self.format_title_artist.setChecked(format_type == 'title_artist')
self.format_artist_title.setChecked(format_type == 'artist_title')
self.dir_input.setText(output_dir)
@@ -138,6 +205,8 @@ class SpotifyFlacGUI(QMainWindow):
def setup_settings_persistence(self):
self.headless_checkbox.stateChanged.connect(
lambda x: self.settings.setValue('headless', bool(x)))
self.service_combo.currentIndexChanged.connect(
lambda i: self.settings.setValue('service', self.service_combo.itemData(i)))
self.format_title_artist.toggled.connect(
lambda x: self.settings.setValue('format', 'title_artist' if x else 'artist_title'))
self.dir_input.textChanged.connect(
@@ -185,24 +254,38 @@ class SpotifyFlacGUI(QMainWindow):
settings_group = QGroupBox("Settings")
settings_layout = QHBoxLayout(settings_group)
settings_layout.setContentsMargins(10, 0, 10, 10)
settings_layout.setSpacing(20)
settings_layout.setSpacing(15)
settings_container = QWidget()
settings_container_layout = QHBoxLayout(settings_container)
settings_container_layout.setContentsMargins(0, 0, 0, 0)
settings_container_layout.setSpacing(20)
settings_container_layout.setSpacing(15)
self.headless_checkbox = QCheckBox("Headless")
self.headless_checkbox.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.headless_checkbox.setChecked(True)
settings_container_layout.addWidget(self.headless_checkbox)
service_widget = QWidget()
service_layout = QHBoxLayout(service_widget)
service_layout.setContentsMargins(0, 0, 0, 0)
service_layout.setSpacing(10)
service_label = QLabel("Service:")
self.service_combo = ServiceComboBox()
self.service_combo.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
service_layout.addWidget(service_label)
service_layout.addWidget(self.service_combo)
settings_container_layout.addWidget(service_widget)
format_widget = QWidget()
format_layout = QHBoxLayout(format_widget)
format_layout.setContentsMargins(0, 0, 0, 0)
format_layout.setSpacing(15)
format_layout.setSpacing(10)
format_label = QLabel("Filename Format:")
format_label = QLabel("Filename:")
self.format_title_artist = QRadioButton("Title - Artist")
self.format_artist_title = QRadioButton("Artist - Title")
self.format_title_artist.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
@@ -323,7 +406,8 @@ class SpotifyFlacGUI(QMainWindow):
self.fetch_button.setEnabled(False)
self.status_label.setText("Fetching track information...")
headless = self.headless_checkbox.isChecked()
self.fetcher = MetadataFetcher(url, headless=headless)
service = self.service_combo.currentData()
self.fetcher = MetadataFetcher(url, headless=headless, service=service)
self.fetcher.finished.connect(self.handle_track_info)
self.fetcher.error.connect(self.handle_fetch_error)
self.fetcher.start()
@@ -415,7 +499,7 @@ class SpotifyFlacGUI(QMainWindow):
self.cancel_button.hide()
self.progress_bar.show()
self.progress_bar.setValue(0)
self.status_label.setText("Downloading...")
self.status_label.setText("Preparing...")
format_type = 'artist_title' if self.format_artist_title.isChecked() else 'title_artist'
self.worker = DownloaderWorker(
@@ -425,10 +509,14 @@ class SpotifyFlacGUI(QMainWindow):
)
self.worker.progress.connect(self.update_progress)
self.worker.status.connect(self.update_status)
self.worker.finished.connect(self.download_finished)
self.worker.error.connect(self.download_error)
self.worker.start()
def update_status(self, status):
self.status_label.setText(status)
def update_progress(self, value):
self.progress_bar.setValue(value)
@@ -457,4 +545,4 @@ def main():
sys.exit(app.exec())
if __name__ == "__main__":
main()
main()