diff --git a/Archived/GetMetadata.py b/Archived/GetMetadata.py
index 1424235..11e8b8c 100644
--- a/Archived/GetMetadata.py
+++ b/Archived/GetMetadata.py
@@ -1,7 +1,7 @@
import asyncio
import zendriver as zd
-async def get_metadata(page):
+async def get_metadata(page, headless=True):
max_attempts = 40
attempts = 0
@@ -14,9 +14,9 @@ async def get_metadata(page):
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;
@@ -82,8 +82,8 @@ async def get_metadata(page):
raise TimeoutError("Timeout")
-async def main():
- browser = await zd.start(headless=False)
+async def main(headless=True):
+ browser = await zd.start(headless=headless)
try:
track_id = "2plbrEY59IikOBgBGLjaoe"
url = f"https://lucida.to/?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F{track_id}&country=auto&to=tidal"
diff --git a/Archived/LucidaDownloader.py b/Archived/LucidaDownloader.py
deleted file mode 100644
index 92c3896..0000000
--- a/Archived/LucidaDownloader.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import requests
-from tqdm import tqdm
-import time
-import os
-import asyncio
-from GetMetadata import main as get_metadata
-
-class TrackDownloader:
- def __init__(self):
- self.client = requests.Session()
- 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'
- }
-
- async def get_track_info(self):
- metadata = await get_metadata()
- return metadata
-
- def sanitize_filename(self, filename):
- invalid_chars = '<>:"/\\|?*'
- for char in invalid_chars:
- filename = filename.replace(char, '')
-
- filename = ' '.join(filename.split())
- filename = filename.replace(' ,', ',')
- filename = filename.replace(',', ', ')
- while ' ' in filename:
- filename = filename.replace(' ', ' ')
- filename = filename.rsplit('.', 1)
- filename[0] = filename[0].strip()
- return '.'.join(filename)
-
- def download(self, metadata, output_dir):
- track_url = metadata['url']
- primary_token = metadata['token']
- expiry = metadata['expiry']
-
- print(f"Starting download for: {track_url}")
-
- initial_request = {
- "account": {"id": "auto", "type": "country"},
- "compat": "false",
- "downscale": "original",
- "handoff": True,
- "metadata": True,
- "private": True,
- "token": {
- "expiry": expiry,
- "primary": primary_token
- },
- "upload": {"enabled": False, "service": "pixeldrain"},
- "url": track_url
- }
-
- response = self.client.post("https://lucida.to/api/load?url=/api/fetch/stream/v2",
- json=initial_request,
- headers=self.headers)
-
- csrf_token = response.cookies.get('csrf_token')
- if csrf_token:
- self.headers['X-CSRF-Token'] = csrf_token
-
- initial_response = response.json()
-
- if not initial_response.get("success", False):
- raise Exception(f"Initial request failed: {initial_response.get('error', 'Unknown error')}")
-
- handoff = initial_response["handoff"]
- server = initial_response["server"]
-
- file_name = f"{metadata['title']} - {metadata['artists']}.flac"
- file_name = self.sanitize_filename(file_name)
-
- completion_url = f"https://{server}.lucida.to/api/fetch/request/{handoff}"
-
- print("Waiting for track processing to complete")
- while True:
- completion_response = self.client.get(completion_url, headers=self.headers).json()
- if completion_response["status"] == "completed":
- break
- elif completion_response["status"] == "error":
- raise Exception(f"API request failed: {completion_response.get('message', 'Unknown error')}")
- time.sleep(1)
-
- download_url = f"https://{server}.lucida.to/api/fetch/request/{handoff}/download"
- print(f"Starting download of: {file_name}")
-
- response = self.client.get(download_url, stream=True, headers=self.headers)
- total_size = int(response.headers.get('content-length', 0))
-
- file_path = os.path.join(output_dir, file_name)
-
- with open(file_path, 'wb') as file, tqdm(
- desc=file_name,
- total=total_size,
- unit='iB',
- unit_scale=True,
- unit_divisor=1024,
- ) as progress_bar:
- for data in response.iter_content(chunk_size=1024):
- size = file.write(data)
- progress_bar.update(size)
-
- print(f"Download completed: {file_path}")
- return file_path
-
-async def main():
- downloader = TrackDownloader()
- output_dir = "."
-
- try:
- metadata = await downloader.get_track_info()
-
- downloaded_file = downloader.download(metadata, output_dir)
- print(f"File downloaded successfully: {downloaded_file}")
- except Exception as e:
- print(f"An error occurred: {str(e)}")
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/SpotiFLAC.py b/SpotiFLAC.py
index 701892e..c4fcdf3 100644
--- a/SpotiFLAC.py
+++ b/SpotiFLAC.py
@@ -1,7 +1,7 @@
import sys
-import asyncio
import os
import time
+from datetime import datetime
import requests
from pathlib import Path
from packaging import version
@@ -11,7 +11,6 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QGroupBox, QComboBox, QDialog, QDialogButtonBox)
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QSettings, QSize, QTimer, QUrl
from PyQt6.QtGui import QIcon, QPixmap, QCursor,QDesktopServices
-from getMetadata import get_metadata
from getTracks import TrackDownloader
class ImageDownloader(QThread):
@@ -31,40 +30,18 @@ class MetadataFetcher(QThread):
finished = pyqtSignal(dict)
error = pyqtSignal(str)
- def __init__(self, url, headless=True, service="tidal", use_fallback=False):
+ def __init__(self, url, service="amazon", use_fallback=False):
super().__init__()
self.url = url
- self.headless_mode = headless
self.service = service
self.use_fallback = use_fallback
self.max_retries = 3
def extract_track_id(self, url):
if "track/" in url:
- return url.split("track/")[1].split("?")[0]
+ return url.split("track/")[1].split("?")[0].split("/")[0]
return None
- async def fetch_metadata(self, track_id):
- import zendriver as zd
- from asyncio import sleep
-
- domain = "lucida.su" if self.use_fallback else "lucida.to"
-
- for attempt in range(self.max_retries):
- try:
- lucida_url = f"https://{domain}/?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)
- return await get_metadata(page)
- finally:
- await browser.stop()
- except Exception as e:
- if "refused" in str(e).lower() and attempt < self.max_retries - 1:
- await sleep(2 * (attempt + 1))
- continue
- raise e
-
def run(self):
try:
track_id = self.extract_track_id(self.url)
@@ -72,11 +49,33 @@ class MetadataFetcher(QThread):
self.error.emit("Invalid Spotify URL")
return
- metadata = asyncio.run(self.fetch_metadata(track_id))
- if metadata:
- self.finished.emit(metadata)
- else:
- self.error.emit("Failed to fetch track metadata")
+ fallback = "su" if self.use_fallback else "to"
+ api_url = f"https://apislucida.vercel.app/{fallback}/{track_id}/{self.service}"
+
+ for attempt in range(self.max_retries):
+ try:
+ response = requests.get(api_url)
+ response.raise_for_status()
+
+ metadata = response.json()
+ formatted_metadata = {
+ 'title': metadata['title'],
+ 'artists': metadata['artists'],
+ 'cover': metadata['coverArtwork'],
+ 'url': metadata['url'],
+ 'token': metadata['token'],
+ 'duration': metadata.get('durationMs', 0),
+ 'release_date': metadata.get('releaseDate', '')
+ }
+
+ self.finished.emit(formatted_metadata)
+ return
+
+ except requests.exceptions.RequestException as e:
+ if attempt < self.max_retries - 1:
+ time.sleep(2 * (attempt + 1))
+ continue
+ raise e
except Exception as e:
error_msg = str(e)
@@ -162,9 +161,8 @@ class ServiceComboBox(QComboBox):
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'}
+ {'id': 'tidal', 'name': 'Tidal', 'icon': 'tidal.png'}
]
for service in services:
@@ -218,7 +216,7 @@ class UpdateDialog(QDialog):
class SpotiFlacGUI(QMainWindow):
def __init__(self):
super().__init__()
- self.current_version = "1.7"
+ self.current_version = "1.8"
self.settings = QSettings('SpotiFlac', 'Settings')
self.setWindowTitle("SpotiFLAC")
self.check_for_updates = self.settings.value('check_for_updates', True, type=bool)
@@ -269,13 +267,11 @@ class SpotiFlacGUI(QMainWindow):
print(f"Error checking for updates: {e}")
def load_settings(self):
- headless = self.settings.value('headless', True, type=bool)
fallback = self.settings.value('fallback', False, type=bool)
- service = self.settings.value('service', 'tidal')
+ service = self.settings.value('service', 'amazon')
format_type = self.settings.value('format', 'title_artist')
output_dir = self.settings.value('output_dir', self.default_music_dir)
- self.headless_checkbox.setChecked(headless)
self.fallback_checkbox.setChecked(fallback)
for i in range(self.service_combo.count()):
@@ -288,8 +284,6 @@ class SpotiFlacGUI(QMainWindow):
self.dir_input.setText(output_dir)
def setup_settings_persistence(self):
- self.headless_checkbox.stateChanged.connect(
- lambda x: self.settings.setValue('headless', bool(x)))
self.fallback_checkbox.stateChanged.connect(
lambda x: self.settings.setValue('fallback', bool(x)))
self.service_combo.currentIndexChanged.connect(
@@ -348,12 +342,7 @@ class SpotiFlacGUI(QMainWindow):
settings_container_layout.setContentsMargins(0, 0, 0, 0)
settings_container_layout.setSpacing(10)
- 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)
-
- self.fallback_checkbox = QCheckBox("Fallback")
+ self.fallback_checkbox = QCheckBox("Fallback Server")
self.fallback_checkbox.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.fallback_checkbox.setChecked(False)
settings_container_layout.addWidget(self.fallback_checkbox)
@@ -377,7 +366,7 @@ class SpotiFlacGUI(QMainWindow):
format_layout.setContentsMargins(0, 0, 0, 0)
format_layout.setSpacing(10)
- format_label = QLabel("Filename:")
+ format_label = QLabel("Filename Format:")
self.format_title_artist = QRadioButton("Title")
self.format_artist_title = QRadioButton("Artist")
self.format_title_artist.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
@@ -529,10 +518,9 @@ class SpotiFlacGUI(QMainWindow):
return
self.fetch_button.setEnabled(False)
self.status_label.setText("Fetching track information...")
- headless = self.headless_checkbox.isChecked()
fallback = self.fallback_checkbox.isChecked()
service = self.service_combo.currentData()
- self.fetcher = MetadataFetcher(url, headless=headless, service=service, use_fallback=fallback)
+ self.fetcher = MetadataFetcher(url, service=service, use_fallback=fallback)
self.fetcher.finished.connect(self.handle_track_info)
self.fetcher.error.connect(self.handle_fetch_error)
self.fetcher.start()
@@ -541,25 +529,43 @@ class SpotiFlacGUI(QMainWindow):
self.metadata = metadata
self.fetch_button.setEnabled(True)
self.title_label.setText(metadata['title'].strip())
- self.artist_label.setText(metadata['artists'].strip())
+
+ artist_text = ""
+
+ artists_list = metadata['artists'].strip().split(",")
+ if len(artists_list) > 1:
+ artist_text += "Artists " + metadata['artists'].strip()
+ else:
+ artist_text += "Artist " + metadata['artists'].strip()
+
+ if metadata.get('release_date'):
+ try:
+ date_obj = datetime.fromisoformat(metadata['release_date'].replace('Z', '+00:00'))
+ formatted_date = date_obj.strftime("%d-%m-%Y")
+ artist_text += f"
Released {formatted_date}"
+ except:
+ if metadata['release_date']:
+ artist_text += f"
Released {metadata['release_date']}"
+
+ if metadata.get('duration'):
+ duration_ms = metadata['duration']
+ minutes = int(duration_ms / 60000)
+ seconds = int((duration_ms % 60000) / 1000)
+ artist_text += f"
Duration {minutes}:{seconds:02d}"
+
+ self.artist_label.setText(artist_text)
+ self.artist_label.setTextFormat(Qt.TextFormat.RichText)
+
self.image_downloader = ImageDownloader(metadata['cover'])
self.image_downloader.finished.connect(self.update_cover_art)
self.image_downloader.start()
+
self.input_widget.hide()
self.track_widget.show()
self.download_button.show()
self.cancel_button.show()
self.update_button.hide()
self.status_label.clear()
- self.adjustWindowHeight()
-
- def adjustWindowHeight(self):
- title_height = self.title_label.sizeHint().height()
- artist_height = self.artist_label.sizeHint().height()
- base_height = 180
- additional_height = max(0, (title_height + artist_height) - 40)
- new_height = min(300, base_height + additional_height)
- self.setFixedHeight(int(new_height))
def update_cover_art(self, image_data):
pixmap = QPixmap()
@@ -677,4 +683,4 @@ def main():
sys.exit(app.exec())
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file
diff --git a/getMetadata.py b/getMetadata.py
deleted file mode 100644
index 424da7c..0000000
--- a/getMetadata.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import asyncio
-import zendriver as zd
-
-async def get_metadata(page, headless=True):
- max_attempts = 40
- attempts = 0
-
- await asyncio.sleep(2)
-
- await page.evaluate("""
- window.downloadInfo = null;
- const originalFetch = window.fetch;
- window.fetch = async function(...args) {
- 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.trim();
- const artists = Array.from(document.querySelectorAll('h2.svelte-6pt9ji a.normal'))
- .map(a => a.textContent.trim())
- .join(', ');
- const cover = document.querySelector('.svelte-6pt9ji .meta.svelte-6pt9ji a').href;
-
- window.downloadInfo = {
- url: payload.url,
- cover: cover,
- title: title,
- artists: artists,
- token: payload.token.primary,
- expiry: payload.token.expiry
- };
- }
- return originalFetch.apply(this, args);
- };
- """)
-
- await page.evaluate("""
- function waitForElement(selector) {
- return new Promise(resolve => {
- if (document.querySelector(selector)) {
- return resolve(document.querySelector(selector));
- }
-
- const observer = new MutationObserver(mutations => {
- if (document.querySelector(selector)) {
- observer.disconnect();
- resolve(document.querySelector(selector));
- }
- });
-
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true
- });
- });
- }
-
- (async () => {
- if (!window.location.hostname.includes('lucida.')) return;
-
- await Promise.race([
- waitForElement('.d1-track button'),
- waitForElement('button[class*="download-button"]')
- ]);
-
- const clickDownloadButton = () => {
- const button = document.querySelector('.d1-track button') ||
- document.querySelector('button[class*="download-button"]');
- if (button) button.click();
- };
-
- clickDownloadButton();
- })();
- """)
-
- while attempts < max_attempts:
- download_info = await page.evaluate("window.downloadInfo")
- if download_info:
- return download_info
-
- await asyncio.sleep(0.5)
- attempts += 1
-
- raise TimeoutError("Timeout")
-
-async def main(headless=True):
- browser = await zd.start(headless=headless)
- try:
- track_id = "2plbrEY59IikOBgBGLjaoe"
- url = f"https://lucida.to/?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F{track_id}&country=auto&to=tidal"
-
- page = await browser.get(url)
- download_info = await get_metadata(page)
- print(download_info)
- return download_info
- finally:
- await browser.stop()
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/getTracks.py b/getTracks.py
index 1cc3246..67872af 100644
--- a/getTracks.py
+++ b/getTracks.py
@@ -2,7 +2,6 @@ import requests
import time
import os
import asyncio
-from getMetadata import main as get_metadata
class TrackDownloader:
def __init__(self, use_fallback=False):
@@ -14,6 +13,7 @@ class TrackDownloader:
self.filename_format = 'title_artist'
self.use_fallback = use_fallback
self.base_domain = "lucida.su" if use_fallback else "lucida.to"
+ self.api_base = "https://apislucida.vercel.app"
def set_progress_callback(self, callback):
self.progress_callback = callback
@@ -28,9 +28,19 @@ class TrackDownloader:
filename = f"{metadata['title']} - {metadata['artists']}.flac"
return self.sanitize_filename(filename)
- async def get_track_info(self):
- metadata = await get_metadata()
- return metadata
+ async def get_track_info(self, track_id, service="amazon", use_fallback=None):
+ if use_fallback is None:
+ use_fallback = self.use_fallback
+
+ fallback = "su" if use_fallback else "to"
+ api_url = f"{self.api_base}/{fallback}/{track_id}/{service}"
+
+ try:
+ response = requests.get(api_url)
+ response.raise_for_status()
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ raise Exception(f"Failed to get track info: {str(e)}")
def sanitize_filename(self, filename):
invalid_chars = '<>:"/\\|?*'
@@ -48,8 +58,8 @@ class TrackDownloader:
def download(self, metadata, output_dir):
track_url = metadata['url']
- primary_token = metadata['token']
- expiry = metadata['expiry']
+ primary_token = metadata['token']['primary']
+ expiry = metadata['token']['expiry']
print(f"Starting download for: {track_url}")
@@ -131,13 +141,15 @@ class TrackDownloader:
async def main():
downloader = TrackDownloader()
output_dir = "."
+ track_id = "2plbrEY59IikOBgBGLjaoe"
+ service = "amazon"
try:
- metadata = await downloader.get_track_info()
+ metadata = await downloader.get_track_info(track_id, service)
downloaded_file = downloader.download(metadata, output_dir)
print(f"File downloaded successfully: {downloaded_file}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
- asyncio.run(main())
+ asyncio.run(main())
\ No newline at end of file
diff --git a/version.json b/version.json
index 8946519..a942332 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "version": "1.7"
+ "version": "1.8"
}