Compare commits

...

5 Commits

Author SHA1 Message Date
afkarxyz 783350fe88 v3.9 2025-07-21 17:28:44 +07:00
afkarxyz 0057d43f46 v3.8 2025-07-14 13:20:14 +07:00
afkarxyz 9928968ffb v3.7 2025-07-13 05:30:59 +07:00
afkarxyz af4f1dd401 v3.7 2025-07-13 05:25:13 +07:00
afkarxyz 3414fadbd3 v3.6 2025-07-08 16:44:11 +07:00
5 changed files with 43 additions and 46 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,3 @@
{ {
"version": "3.5" "version": "3.8"
} }