Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 783350fe88 | |||
| 0057d43f46 | |||
| 9928968ffb | |||
| af4f1dd401 | |||
| 3414fadbd3 |
@@ -6,7 +6,7 @@
|
|||||||
<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.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v3.5/SpotiFLAC.exe)
|
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v3.8/SpotiFLAC.exe)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
+20
-10
@@ -76,6 +76,8 @@ class DownloadWorker(QThread):
|
|||||||
def get_formatted_filename(self, track):
|
def get_formatted_filename(self, track):
|
||||||
if self.filename_format == "artist_title":
|
if self.filename_format == "artist_title":
|
||||||
filename = f"{track.artists} - {track.title}.flac"
|
filename = f"{track.artists} - {track.title}.flac"
|
||||||
|
elif self.filename_format == "title_only":
|
||||||
|
filename = f"{track.title}.flac"
|
||||||
else:
|
else:
|
||||||
filename = f"{track.title} - {track.artists}.flac"
|
filename = f"{track.title} - {track.artists}.flac"
|
||||||
return re.sub(r'[<>:"/\\|?*]', '_', filename)
|
return re.sub(r'[<>:"/\\|?*]', '_', filename)
|
||||||
@@ -503,7 +505,7 @@ class QobuzRegionComboBox(QComboBox):
|
|||||||
class SpotiFLACGUI(QWidget):
|
class SpotiFLACGUI(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.current_version = "3.6"
|
self.current_version = "3.9"
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
|
|
||||||
@@ -544,12 +546,11 @@ class SpotiFLACGUI(QWidget):
|
|||||||
if dialog.disable_check.isChecked():
|
if dialog.disable_check.isChecked():
|
||||||
self.settings.setValue('check_for_updates', False)
|
self.settings.setValue('check_for_updates', False)
|
||||||
self.check_for_updates = False
|
self.check_for_updates = False
|
||||||
|
|
||||||
if result == QDialog.DialogCode.Accepted:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases"))
|
QDesktopServices.openUrl(QUrl("https://github.com/afkarxyz/SpotiFLAC/releases"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error checking for updates: {e}")
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_duration(ms):
|
def format_duration(ms):
|
||||||
@@ -788,7 +789,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
|
|
||||||
format_layout = QHBoxLayout()
|
format_layout = QHBoxLayout()
|
||||||
format_label = QLabel('Filename Format:')
|
format_label = QLabel('Filename Format:')
|
||||||
|
|
||||||
self.format_group = QButtonGroup(self)
|
self.format_group = QButtonGroup(self)
|
||||||
self.title_artist_radio = QRadioButton('Title - Artist')
|
self.title_artist_radio = QRadioButton('Title - Artist')
|
||||||
self.title_artist_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.title_artist_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
@@ -798,17 +798,25 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.artist_title_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.artist_title_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.artist_title_radio.toggled.connect(self.save_filename_format)
|
self.artist_title_radio.toggled.connect(self.save_filename_format)
|
||||||
|
|
||||||
|
self.title_only_radio = QRadioButton('Title')
|
||||||
|
self.title_only_radio.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
self.title_only_radio.toggled.connect(self.save_filename_format)
|
||||||
|
|
||||||
if hasattr(self, 'filename_format') and self.filename_format == "artist_title":
|
if hasattr(self, 'filename_format') and self.filename_format == "artist_title":
|
||||||
self.artist_title_radio.setChecked(True)
|
self.artist_title_radio.setChecked(True)
|
||||||
|
elif hasattr(self, 'filename_format') and self.filename_format == "title_only":
|
||||||
|
self.title_only_radio.setChecked(True)
|
||||||
else:
|
else:
|
||||||
self.title_artist_radio.setChecked(True)
|
self.title_artist_radio.setChecked(True)
|
||||||
|
|
||||||
self.format_group.addButton(self.title_artist_radio)
|
self.format_group.addButton(self.title_artist_radio)
|
||||||
self.format_group.addButton(self.artist_title_radio)
|
self.format_group.addButton(self.artist_title_radio)
|
||||||
|
self.format_group.addButton(self.title_only_radio)
|
||||||
|
|
||||||
format_layout.addWidget(format_label)
|
format_layout.addWidget(format_label)
|
||||||
format_layout.addWidget(self.title_artist_radio)
|
format_layout.addWidget(self.title_artist_radio)
|
||||||
format_layout.addWidget(self.artist_title_radio)
|
format_layout.addWidget(self.artist_title_radio)
|
||||||
|
format_layout.addWidget(self.title_only_radio)
|
||||||
format_layout.addStretch()
|
format_layout.addStretch()
|
||||||
file_layout.addLayout(format_layout)
|
file_layout.addLayout(format_layout)
|
||||||
|
|
||||||
@@ -931,7 +939,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.6 | July 2025")
|
footer_label = QLabel("v3.9 | 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)
|
||||||
|
|
||||||
@@ -982,7 +990,12 @@ class SpotiFLACGUI(QWidget):
|
|||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
|
|
||||||
def save_filename_format(self):
|
def save_filename_format(self):
|
||||||
self.filename_format = "artist_title" if self.artist_title_radio.isChecked() else "title_artist"
|
if self.artist_title_radio.isChecked():
|
||||||
|
self.filename_format = "artist_title"
|
||||||
|
elif self.title_only_radio.isChecked():
|
||||||
|
self.filename_format = "title_only"
|
||||||
|
else:
|
||||||
|
self.filename_format = "title_artist"
|
||||||
self.settings.setValue('filename_format', self.filename_format)
|
self.settings.setValue('filename_format', self.filename_format)
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
|
|
||||||
@@ -1387,7 +1400,6 @@ class SpotiFLACGUI(QWidget):
|
|||||||
for i, track in enumerate(self.tracks, 1):
|
for i, track in enumerate(self.tracks, 1):
|
||||||
if self.is_playlist:
|
if self.is_playlist:
|
||||||
track.track_number = i
|
track.track_number = i
|
||||||
|
|
||||||
duration = self.format_duration(track.duration_ms)
|
duration = self.format_duration(track.duration_ms)
|
||||||
display_text = f"{i}. {track.title} - {track.artists} • {duration}"
|
display_text = f"{i}. {track.title} - {track.artists} • {duration}"
|
||||||
list_item = self.track_list.item(i - 1)
|
list_item = self.track_list.item(i - 1)
|
||||||
@@ -1412,13 +1424,11 @@ class SpotiFLACGUI(QWidget):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
import os
|
|
||||||
os.system("chcp 65001 > nul")
|
|
||||||
import io
|
import io
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not set UTF-8 encoding: {e}")
|
pass
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
ex = SpotiFLACGUI()
|
ex = SpotiFLACGUI()
|
||||||
|
|||||||
+17
-4
@@ -13,7 +13,20 @@ def get_random_user_agent():
|
|||||||
return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
|
return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{randrange(11, 15)}_{randrange(4, 9)}) AppleWebKit/{randrange(530, 537)}.{randrange(30, 37)} (KHTML, like Gecko) Chrome/{randrange(80, 105)}.0.{randrange(3000, 4500)}.{randrange(60, 125)} Safari/{randrange(530, 537)}.{randrange(30, 36)}"
|
||||||
|
|
||||||
def generate_totp():
|
def generate_totp():
|
||||||
secret_cipher = [61, 110, 58, 98, 35, 79, 117, 69, 102, 72, 92, 102, 69, 93, 41, 101, 42, 75]
|
url = "https://raw.githubusercontent.com/Thereallo1026/spotify-secrets/refs/heads/main/secrets/secretBytes.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=10)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise Exception(f"Failed to fetch TOTP secrets from GitHub. Status: {resp.status_code}")
|
||||||
|
secrets_list = resp.json()
|
||||||
|
|
||||||
|
latest_entry = max(secrets_list, key=lambda x: x["version"])
|
||||||
|
version = latest_entry["version"]
|
||||||
|
secret_cipher = latest_entry["secret"]
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Failed to fetch secrets from GitHub: {str(e)}")
|
||||||
|
|
||||||
processed = [byte ^ ((i % 33) + 9) for i, byte in enumerate(secret_cipher)]
|
processed = [byte ^ ((i % 33) + 9) for i, byte in enumerate(secret_cipher)]
|
||||||
processed_str = "".join(map(str, processed))
|
processed_str = "".join(map(str, processed))
|
||||||
utf8_bytes = processed_str.encode('utf-8')
|
utf8_bytes = processed_str.encode('utf-8')
|
||||||
@@ -36,7 +49,7 @@ def generate_totp():
|
|||||||
server_time = data.get("serverTime")
|
server_time = data.get("serverTime")
|
||||||
if server_time is None:
|
if server_time is None:
|
||||||
raise Exception("Failed to fetch server time from Spotify")
|
raise Exception("Failed to fetch server time from Spotify")
|
||||||
return totp, server_time
|
return totp, server_time, version
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Error getting server time: {str(e)}")
|
raise Exception(f"Error getting server time: {str(e)}")
|
||||||
|
|
||||||
@@ -110,7 +123,7 @@ def get_json_from_api(api_url, access_token):
|
|||||||
|
|
||||||
def get_access_token():
|
def get_access_token():
|
||||||
try:
|
try:
|
||||||
totp, server_time = generate_totp()
|
totp, server_time, totp_version = generate_totp()
|
||||||
otp_code = totp.at(int(server_time))
|
otp_code = totp.at(int(server_time))
|
||||||
timestamp_ms = int(time.time() * 1000)
|
timestamp_ms = int(time.time() * 1000)
|
||||||
|
|
||||||
@@ -119,7 +132,7 @@ def get_access_token():
|
|||||||
'productType': 'web-player',
|
'productType': 'web-player',
|
||||||
'totp': otp_code,
|
'totp': otp_code,
|
||||||
'totpServerTime': server_time,
|
'totpServerTime': server_time,
|
||||||
'totpVer': '8',
|
'totpVer': str(totp_version),
|
||||||
'sTime': server_time,
|
'sTime': server_time,
|
||||||
'cTime': timestamp_ms,
|
'cTime': timestamp_ms,
|
||||||
'buildVer': 'web-player_2025-07-02_1720000000000_12345678',
|
'buildVer': 'web-player_2025-07-02_1720000000000_12345678',
|
||||||
|
|||||||
+2
-28
@@ -2,7 +2,6 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
import httpx
|
import httpx
|
||||||
from mutagen.flac import FLAC, Picture
|
from mutagen.flac import FLAC, Picture
|
||||||
@@ -24,22 +23,11 @@ class TidalDownloader:
|
|||||||
self.progress_callback = ProgressCallback()
|
self.progress_callback = ProgressCallback()
|
||||||
self.client_id = "zU4XHVVkc2tDPo4t"
|
self.client_id = "zU4XHVVkc2tDPo4t"
|
||||||
self.client_secret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4="
|
self.client_secret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4="
|
||||||
self.temp_dir = tempfile.gettempdir()
|
|
||||||
self.token_path = os.path.join(self.temp_dir, "tidal_token.json")
|
|
||||||
self.access_token = None
|
|
||||||
self._load_token()
|
|
||||||
|
|
||||||
def set_progress_callback(self, callback):
|
def set_progress_callback(self, callback):
|
||||||
self.progress_callback = callback
|
self.progress_callback = callback
|
||||||
|
|
||||||
def _load_token(self):
|
|
||||||
if os.path.exists(self.token_path):
|
|
||||||
try:
|
|
||||||
with open(self.token_path, "r") as tok:
|
|
||||||
token = json.loads(tok.read())
|
|
||||||
self.access_token = token.get("access_token")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def sanitize_filename(self, filename):
|
def sanitize_filename(self, filename):
|
||||||
if not filename:
|
if not filename:
|
||||||
@@ -48,9 +36,6 @@ class TidalDownloader:
|
|||||||
return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track"
|
return re.sub(r'\s+', ' ', sanitized).strip() or "Unnamed Track"
|
||||||
|
|
||||||
async def get_access_token(self):
|
async def get_access_token(self):
|
||||||
if self.access_token:
|
|
||||||
return self.access_token
|
|
||||||
|
|
||||||
refresh_url = "https://auth.tidal.com/v1/oauth2/token"
|
refresh_url = "https://auth.tidal.com/v1/oauth2/token"
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@@ -68,18 +53,7 @@ class TidalDownloader:
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
token_data = response.json()
|
token_data = response.json()
|
||||||
new_token = token_data.get("access_token")
|
return token_data.get("access_token")
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.token_path, "w") as f:
|
|
||||||
json.dump({
|
|
||||||
"access_token": new_token
|
|
||||||
}, f)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.access_token = new_token
|
|
||||||
return new_token
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "3.5"
|
"version": "3.8"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user