Compare commits

...

4 Commits

Author SHA1 Message Date
afkarxyz 9bddeab0d1 v4.1 2025-07-24 06:14:42 +07:00
afkarxyz 03a30ee09a v4.0 2025-07-22 14:03:41 +07:00
afkarxyz 2d908e2f75 v4.0 2025-07-22 08:02:54 +07:00
afkarxyz e8f7bf7313 v4.0 2025-07-22 07:59:28 +07:00
4 changed files with 129 additions and 73 deletions
+8 -2
View File
@@ -3,10 +3,16 @@
![SpotiFLAC](https://github.com/user-attachments/assets/b4c4f403-edbd-4a71-b74b-c7d433d47d06) ![SpotiFLAC](https://github.com/user-attachments/assets/b4c4f403-edbd-4a71-b74b-c7d433d47d06)
<div align="center"> <div align="center">
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Qobuz & Tidal. <b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Qobuz, Tidal & Deezer.
</div> </div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v3.9/SpotiFLAC.exe) ### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.0/SpotiFLAC.exe)
#
> [!Important]
> - Requires **Google Chrome, Chromium, Microsoft Edge,** or **Brave** to use `Deezer`
> - If after **Cloudflare** verification nothing happens, use a `VPN`, your country is likely blocked by `corsproxy.io`
## Screenshots ## Screenshots
+92 -59
View File
@@ -58,7 +58,7 @@ class DownloadWorker(QThread):
progress = pyqtSignal(str, int) progress = pyqtSignal(str, int)
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, service="tidal", qobuz_region="us", deezer_speed=5): use_album_subfolders=False, service="tidal", qobuz_region="us"):
super().__init__() super().__init__()
self.tracks = tracks self.tracks = tracks
self.outpath = outpath self.outpath = outpath
@@ -71,7 +71,6 @@ class DownloadWorker(QThread):
self.use_album_subfolders = use_album_subfolders self.use_album_subfolders = use_album_subfolders
self.service = service self.service = service
self.qobuz_region = qobuz_region self.qobuz_region = qobuz_region
self.deezer_speed = deezer_speed
self.is_paused = False self.is_paused = False
self.is_stopped = False self.is_stopped = False
self.failed_tracks = [] self.failed_tracks = []
@@ -218,7 +217,7 @@ class DownloadWorker(QThread):
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
success = loop.run_until_complete(downloader.download_by_isrc(track.isrc, track_outpath, self.deezer_speed)) success = loop.run_until_complete(downloader.download_by_isrc(track.isrc, track_outpath))
if success: if success:
safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '-', '_')).rstrip() safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '-', '_')).rstrip()
@@ -584,8 +583,9 @@ class QobuzRegionComboBox(QComboBox):
class SpotiFLACGUI(QWidget): class SpotiFLACGUI(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.current_version = "4.0" self.current_version = "4.1"
self.tracks = [] self.tracks = []
self.all_tracks = []
self.reset_state() self.reset_state()
self.settings = QSettings('SpotiFLAC', 'Settings') self.settings = QSettings('SpotiFLAC', 'Settings')
@@ -597,7 +597,6 @@ class SpotiFLACGUI(QWidget):
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.qobuz_region = self.settings.value('qobuz_region', 'us') self.qobuz_region = self.settings.value('qobuz_region', 'us')
self.deezer_speed = self.settings.value('deezer_speed', 7.5, type=float)
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.elapsed_time = QTime(0, 0, 0) self.elapsed_time = QTime(0, 0, 0)
@@ -640,6 +639,7 @@ class SpotiFLACGUI(QWidget):
def reset_state(self): def reset_state(self):
self.tracks.clear() self.tracks.clear()
self.all_tracks.clear()
self.is_album = False self.is_album = False
self.is_playlist = False self.is_playlist = False
self.is_single_track = False self.is_single_track = False
@@ -655,11 +655,15 @@ class SpotiFLACGUI(QWidget):
self.pause_resume_btn.setText('Pause') self.pause_resume_btn.setText('Pause')
self.reset_info_widget() self.reset_info_widget()
self.hide_track_buttons() self.hide_track_buttons()
if hasattr(self, 'search_input'):
self.search_input.clear()
if hasattr(self, 'search_widget'):
self.search_widget.hide()
def initUI(self): def initUI(self):
self.setWindowTitle('SpotiFLAC') self.setWindowTitle('SpotiFLAC')
self.setFixedWidth(650) self.setFixedWidth(650)
self.setFixedHeight(350) self.setMinimumHeight(350)
icon_path = os.path.join(os.path.dirname(__file__), "icon.svg") icon_path = os.path.join(os.path.dirname(__file__), "icon.svg")
if os.path.exists(icon_path): if os.path.exists(icon_path):
@@ -692,6 +696,27 @@ class SpotiFLACGUI(QWidget):
spotify_layout.addWidget(self.fetch_btn) spotify_layout.addWidget(self.fetch_btn)
self.main_layout.addLayout(spotify_layout) self.main_layout.addLayout(spotify_layout)
def filter_tracks(self):
search_text = self.search_input.text().lower().strip()
if not search_text:
self.tracks = self.all_tracks.copy()
else:
self.tracks = [
track for track in self.all_tracks
if (search_text in track.title.lower() or
search_text in track.artists.lower() or
search_text in track.album.lower())
]
self.update_track_list_display()
def update_track_list_display(self):
self.track_list.clear()
for i, track in enumerate(self.tracks, 1):
duration = self.format_duration(track.duration_ms)
self.track_list.addItem(f"{i}. {track.title} - {track.artists}{duration}")
def browse_output(self): def browse_output(self):
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory") directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
if directory: if directory:
@@ -760,10 +785,37 @@ class SpotiFLACGUI(QWidget):
text_info_layout.addStretch() text_info_layout.addStretch()
info_layout.addLayout(text_info_layout, 1) info_layout.addLayout(text_info_layout, 1)
self.setup_search_widget()
info_layout.addWidget(self.search_widget)
self.info_widget.setLayout(info_layout) self.info_widget.setLayout(info_layout)
self.info_widget.setFixedHeight(100) self.info_widget.setFixedHeight(100)
self.info_widget.hide() self.info_widget.hide()
def setup_search_widget(self):
self.search_widget = QWidget()
search_layout = QVBoxLayout()
search_layout.setContentsMargins(10, 0, 0, 0)
search_layout.addStretch()
search_input_layout = QHBoxLayout()
search_input_layout.addStretch()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Search...")
self.search_input.setClearButtonEnabled(True)
self.search_input.textChanged.connect(self.filter_tracks)
self.search_input.setFixedWidth(250)
search_input_layout.addWidget(self.search_input)
search_layout.addLayout(search_input_layout)
self.search_widget.setLayout(search_layout)
self.search_widget.hide()
def setup_track_buttons(self): def setup_track_buttons(self):
self.btn_layout = QHBoxLayout() self.btn_layout = QHBoxLayout()
self.download_selected_btn = QPushButton('Download Selected') self.download_selected_btn = QPushButton('Download Selected')
@@ -947,17 +999,7 @@ class SpotiFLACGUI(QWidget):
region_label.hide() region_label.hide()
self.qobuz_region_dropdown.hide() self.qobuz_region_dropdown.hide()
self.deezer_speed_label = QLabel('Speed:')
self.deezer_speed_dropdown = QComboBox()
self.deezer_speed_dropdown.addItem('Fast (5s)', 5)
self.deezer_speed_dropdown.addItem('Normal (7.5s)', 7.5)
self.deezer_speed_dropdown.addItem('Slow (10s)', 10)
self.deezer_speed_dropdown.currentIndexChanged.connect(self.save_deezer_speed_setting)
service_fallback_layout.addWidget(self.deezer_speed_label)
service_fallback_layout.addWidget(self.deezer_speed_dropdown)
self.deezer_speed_label.hide()
self.deezer_speed_dropdown.hide()
service_fallback_layout.addStretch() service_fallback_layout.addStretch()
auth_layout.addLayout(service_fallback_layout) auth_layout.addLayout(service_fallback_layout)
@@ -976,10 +1018,7 @@ class SpotiFLACGUI(QWidget):
self.qobuz_region_dropdown.setCurrentIndex(i) self.qobuz_region_dropdown.setCurrentIndex(i)
break break
for i in range(self.deezer_speed_dropdown.count()):
if self.deezer_speed_dropdown.itemData(i) == self.deezer_speed:
self.deezer_speed_dropdown.setCurrentIndex(i)
break
self.update_service_ui() self.update_service_ui()
@@ -1036,7 +1075,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("v4.0 | July 2025") footer_label = QLabel("v4.1 | July 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)
@@ -1064,20 +1103,14 @@ class SpotiFLACGUI(QWidget):
if region_label: if region_label:
region_label.show() region_label.show()
self.qobuz_region_dropdown.show() self.qobuz_region_dropdown.show()
self.deezer_speed_label.hide()
self.deezer_speed_dropdown.hide()
elif service == "deezer": elif service == "deezer":
if region_label: if region_label:
region_label.hide() region_label.hide()
self.qobuz_region_dropdown.hide() self.qobuz_region_dropdown.hide()
self.deezer_speed_label.show()
self.deezer_speed_dropdown.show()
else: else:
if region_label: if region_label:
region_label.hide() region_label.hide()
self.qobuz_region_dropdown.hide() self.qobuz_region_dropdown.hide()
self.deezer_speed_label.hide()
self.deezer_speed_dropdown.hide()
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())
@@ -1109,12 +1142,7 @@ class SpotiFLACGUI(QWidget):
self.settings.sync() self.settings.sync()
self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}") self.log_output.append(f"Qobuz region setting saved: {self.qobuz_region_dropdown.currentText()}")
def save_deezer_speed_setting(self):
speed = self.deezer_speed_dropdown.currentData()
self.deezer_speed = speed
self.settings.setValue('deezer_speed', speed)
self.settings.sync()
self.log_output.append(f"Deezer speed setting saved: {self.deezer_speed_dropdown.currentText()}")
def save_settings(self): def save_settings(self):
self.settings.setValue('output_path', self.output_dir.text().strip()) self.settings.setValue('output_path', self.output_dir.text().strip())
@@ -1169,7 +1197,7 @@ class SpotiFLACGUI(QWidget):
def handle_track_metadata(self, track_data): def handle_track_metadata(self, track_data):
track_id = track_data["external_urls"].split("/")[-1] track_id = track_data["external_urls"].split("/")[-1]
self.tracks = [Track( track = Track(
external_urls=track_data["external_urls"], external_urls=track_data["external_urls"],
title=track_data["name"], title=track_data["name"],
artists=track_data["artists"], artists=track_data["artists"],
@@ -1178,7 +1206,10 @@ class SpotiFLACGUI(QWidget):
duration_ms=track_data.get("duration_ms", 0), duration_ms=track_data.get("duration_ms", 0),
id=track_id, id=track_id,
isrc=track_data.get("isrc", "") isrc=track_data.get("isrc", "")
)] )
self.tracks = [track]
self.all_tracks = [track]
self.is_single_track = True self.is_single_track = True
self.is_album = self.is_playlist = False self.is_album = self.is_playlist = False
self.album_or_playlist_name = f"{self.tracks[0].title} - {self.tracks[0].artists}" self.album_or_playlist_name = f"{self.tracks[0].title} - {self.tracks[0].artists}"
@@ -1209,7 +1240,8 @@ class SpotiFLACGUI(QWidget):
id=track_id, id=track_id,
isrc=track.get("isrc", "") isrc=track.get("isrc", "")
)) ))
self.all_tracks = self.tracks.copy()
self.is_album = True self.is_album = True
self.is_playlist = self.is_single_track = False self.is_playlist = self.is_single_track = False
@@ -1239,7 +1271,8 @@ class SpotiFLACGUI(QWidget):
id=track_id, id=track_id,
isrc=track.get("isrc", "") isrc=track.get("isrc", "")
)) ))
self.all_tracks = self.tracks.copy()
self.is_playlist = True self.is_playlist = True
self.is_album = self.is_single_track = False self.is_album = self.is_single_track = False
@@ -1256,10 +1289,10 @@ class SpotiFLACGUI(QWidget):
self.track_list.setVisible(not self.is_single_track) self.track_list.setVisible(not self.is_single_track)
if not self.is_single_track: if not self.is_single_track:
self.track_list.clear() self.search_widget.show()
for i, track in enumerate(self.tracks, 1): self.update_track_list_display()
duration = self.format_duration(track.duration_ms) else:
self.track_list.addItem(f"{i}. {track.title} - {track.artists}{duration}") self.search_widget.hide()
self.update_info_widget(metadata) self.update_info_widget(metadata)
@@ -1365,13 +1398,14 @@ class SpotiFLACGUI(QWidget):
if not selected_items: if not selected_items:
self.log_output.append('Warning: Please select tracks to download.') self.log_output.append('Warning: Please select tracks to download.')
return return
self.download_tracks([self.track_list.row(item) for item in selected_items]) selected_indices = [self.track_list.row(item) for item in selected_items]
self.download_tracks(selected_indices)
def download_all(self): def download_all(self):
if self.is_single_track: if self.is_single_track:
self.download_tracks([0]) self.download_tracks([0])
else: else:
self.download_tracks(range(self.track_list.count())) self.download_tracks(range(len(self.tracks)))
def download_tracks(self, indices): def download_tracks(self, indices):
self.log_output.clear() self.log_output.clear()
@@ -1397,8 +1431,6 @@ class SpotiFLACGUI(QWidget):
def start_download_worker(self, tracks_to_download, outpath): def start_download_worker(self, tracks_to_download, outpath):
service = self.service_dropdown.currentData() service = self.service_dropdown.currentData()
qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us" qobuz_region = self.qobuz_region_dropdown.currentData() if service == "qobuz" else "us"
deezer_speed = self.deezer_speed_dropdown.currentData() if service == "deezer" else 7.5
self.worker = DownloadWorker( self.worker = DownloadWorker(
tracks_to_download, tracks_to_download,
@@ -1411,8 +1443,7 @@ class SpotiFLACGUI(QWidget):
self.use_track_numbers, self.use_track_numbers,
self.use_album_subfolders, self.use_album_subfolders,
service, service,
qobuz_region, qobuz_region
deezer_speed
) )
self.worker.finished.connect(self.on_download_finished) self.worker.finished.connect(self.on_download_finished)
self.worker.progress.connect(self.update_progress) self.worker.progress.connect(self.update_progress)
@@ -1495,20 +1526,22 @@ class SpotiFLACGUI(QWidget):
def remove_selected_tracks(self): def remove_selected_tracks(self):
if not self.is_single_track: if not self.is_single_track:
selected_indices = sorted([self.track_list.row(item) for item in self.track_list.selectedItems()], reverse=True) selected_items = self.track_list.selectedItems()
selected_indices = [self.track_list.row(item) for item in selected_items]
for index in selected_indices: tracks_to_remove = [self.tracks[i] for i in selected_indices]
self.track_list.takeItem(index)
self.tracks.pop(index)
for i, track in enumerate(self.tracks, 1): for track in tracks_to_remove:
if self.is_playlist: if track in self.tracks:
self.tracks.remove(track)
if track in self.all_tracks:
self.all_tracks.remove(track)
if self.is_playlist:
for i, track in enumerate(self.all_tracks, 1):
track.track_number = i track.track_number = i
duration = self.format_duration(track.duration_ms)
display_text = f"{i}. {track.title} - {track.artists}{duration}" self.update_track_list_display()
list_item = self.track_list.item(i - 1)
if list_item:
list_item.setText(display_text)
def clear_tracks(self): def clear_tracks(self):
self.reset_state() self.reset_state()
+28 -11
View File
@@ -2,10 +2,7 @@ import requests
import asyncio import asyncio
import os import os
import sys import sys
from urllib.parse import urlparse
from mutagen.flac import FLAC from mutagen.flac import FLAC
from mutagen.id3 import ID3NoHeaderError
import deezmate
class DeezerDownloader: class DeezerDownloader:
def __init__(self): def __init__(self):
@@ -128,7 +125,7 @@ class DeezerDownloader:
except Exception as e: except Exception as e:
print(f"Error embedding metadata: {e}") print(f"Error embedding metadata: {e}")
async def download_by_isrc(self, isrc, output_dir=".", initial_delay=7.5): async def download_by_isrc(self, isrc, output_dir="."):
print(f"Fetching track info for ISRC: {isrc}") print(f"Fetching track info for ISRC: {isrc}")
track_data = self.get_track_by_isrc(isrc) track_data = self.get_track_by_isrc(isrc)
@@ -139,16 +136,36 @@ class DeezerDownloader:
metadata = self.extract_metadata(track_data) metadata = self.extract_metadata(track_data)
print(f"Found track: {metadata.get('artists', 'Unknown')} - {metadata.get('title', 'Unknown')}") print(f"Found track: {metadata.get('artists', 'Unknown')} - {metadata.get('title', 'Unknown')}")
deezer_link = metadata.get('deezer_link') track_id = track_data.get('id')
if not deezer_link: if not track_id:
print("No Deezer link found in track data") print("No track ID found in Deezer API response")
return False return False
print(f"Using Deezer link: {deezer_link}") print(f"Using track ID: {track_id}")
flac_url = await deezmate.main(deezer_link, initial_delay) api_url = f"https://api.deezmate.com/dl/{track_id}"
if not flac_url: print(f"Requesting download links from: {api_url}")
print("Failed to get download URL from deezmate")
try:
response = self.session.get(api_url)
response.raise_for_status()
api_data = response.json()
if not api_data.get('success'):
print("API request failed")
return False
links = api_data.get('links', {})
flac_url = links.get('flac')
if not flac_url:
print("No FLAC download link found in API response")
return False
print(f"Successfully obtained FLAC download URL")
except Exception as e:
print(f"Error getting download URL from API: {e}")
return False return False
print("Downloading FLAC file...") print("Downloading FLAC file...")
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "3.9" "version": "4.0"
} }