Compare commits

..

24 Commits

Author SHA1 Message Date
afkarxyz 711c5a98d3 v5.4 2025-11-17 12:48:04 +07:00
afkarxyz fd949a17f0 Add tidal.json with a list of sites 2025-11-17 11:49:38 +07:00
afkarxyz 1c0de8b3ac Merge pull request #69 from MaitreGEEK/patch-1
Update download link to latest release
2025-10-22 13:48:50 +07:00
MaîtreGEEK b653a8ca41 Update download link to latest release
In that way when you release a new version you don't have to modify the README
2025-10-22 08:46:20 +02:00
afkarxyz 2d0c174c50 v5.3 2025-10-22 05:17:57 +07:00
afkarxyz c63eeccc55 v5.3 2025-10-22 05:17:22 +07:00
afkarxyz a620c16b1c v5.3 2025-10-22 05:13:44 +07:00
afkarxyz cf27ae098d v5.2 2025-10-21 03:26:31 +07:00
afkarxyz a0c60a473a . 2025-10-21 03:26:01 +07:00
afkarxyz 25c5a4d175 v5.2 2025-10-21 03:23:28 +07:00
afkarxyz 33a6137f75 v5.1 2025-10-21 02:16:31 +07:00
afkarxyz b4fcb6bca6 v5.1 2025-10-21 02:11:41 +07:00
afkarxyz 5ab19a6d37 . 2025-10-21 02:10:50 +07:00
afkarxyz 8547e6d410 v5.0 2025-10-13 05:37:10 +07:00
afkarxyz 17666d8027 Update version.json 2025-10-13 05:16:51 +07:00
afkarxyz ab208482ca v5.0 2025-10-13 05:09:53 +07:00
afkarxyz 76e02d77e8 v5.0 2025-10-13 05:05:01 +07:00
afkarxyz 75cc4543ad Merge pull request #65 from petacz/patch-1
Use Embedded ISRC Tags to check for existing files
2025-10-13 04:55:34 +07:00
Petr V 0b468c4b60 Use Embedded ISRC Tags to check for existing files 2025-10-12 19:53:28 +02:00
afkarxyz 87a6a778f7 v4.9 2025-10-12 00:30:15 +07:00
afkarxyz ef893ab9f4 v4.9 2025-10-12 00:26:01 +07:00
afkarxyz 3eda3245ca v4.9 2025-10-12 00:23:26 +07:00
afkarxyz f6f238361c v4.9 2025-10-12 00:22:30 +07:00
afkarxyz 998730bbb3 v4.8 2025-10-11 18:18:27 +07:00
16 changed files with 810 additions and 183 deletions
+5 -5
View File
@@ -6,17 +6,17 @@
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal & Deezer. <b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal & Deezer.
</div> </div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v4.7/SpotiFLAC.exe) ### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/latest/download/SpotiFLAC.exe)
## Screenshots ## Screenshots
![image](https://github.com/user-attachments/assets/180b8322-ce2d-4842-a5dd-ac4d7b7a5efa) ![image](https://github.com/user-attachments/assets/416fca55-b885-45c0-af2a-b8b7ce5d5ef3)
![image](https://github.com/user-attachments/assets/3f84d53b-2da1-4488-986c-772b82832f2d) ![image](https://github.com/user-attachments/assets/f9b11da4-dbc3-435e-8954-5627ebe2ccdc)
![image](https://github.com/user-attachments/assets/f604dc04-4ee6-4084-b314-0be7cd5d7ef9) ![image](https://github.com/user-attachments/assets/7507e58d-e228-4edf-adf7-675731731019)
![image](https://github.com/user-attachments/assets/1c3beda2-236b-4452-8afd-a2dfedf389e5) ![image](https://github.com/user-attachments/assets/169da4f1-7b8a-4d50-b72e-c2fe3c51976e)
## Lossless Audio Check ## Lossless Audio Check
+518 -131
View File
File diff suppressed because it is too large Load Diff
+17 -3
View File
@@ -1,5 +1,6 @@
from time import sleep from time import sleep
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
from pathlib import Path
import requests import requests
import json import json
import time import time
@@ -14,19 +15,32 @@ def get_random_user_agent():
# https://github.com/xyloflake/spot-secrets-go # https://github.com/xyloflake/spot-secrets-go
def generate_totp(): def generate_totp():
url = "https://raw.githubusercontent.com/afkarxyz/secretBytes/refs/heads/main/secrets/secretBytes.json" local_path = Path.home() / ".spotify-secret" / "secretBytes.json"
used_local = False
try: try:
url = "https://raw.githubusercontent.com/afkarxyz/secretBytes/refs/heads/main/secrets/secretBytes.json"
resp = requests.get(url, timeout=10) resp = requests.get(url, timeout=10)
if resp.status_code != 200: if resp.status_code != 200:
raise Exception(f"Failed to fetch TOTP secrets from GitHub. Status: {resp.status_code}") raise Exception(f"GitHub fetch failed with status: {resp.status_code}")
secrets_list = resp.json() secrets_list = resp.json()
except Exception as github_error:
try:
if local_path.exists():
with open(local_path, 'r') as f:
secrets_list = json.load(f)
used_local = True
else:
raise Exception(f"GitHub failed ({github_error}) and no local file found at {local_path}")
except Exception as local_error:
raise Exception(f"Failed to fetch secrets from both GitHub and local: {local_error}")
try:
latest_entry = max(secrets_list, key=lambda x: x["version"]) latest_entry = max(secrets_list, key=lambda x: x["version"])
version = latest_entry["version"] version = latest_entry["version"]
secret_cipher = latest_entry["secret"] secret_cipher = latest_entry["secret"]
except Exception as e: except Exception as e:
raise Exception(f"Failed to fetch secrets from GitHub: {str(e)}") raise Exception(f"Failed to process secrets: {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))
+95
View File
@@ -0,0 +1,95 @@
import json
import time
from pathlib import Path
from DrissionPage import ChromiumPage, ChromiumOptions
def summarise(caps):
real = {}
for cap in caps:
sec = cap.get("secret")
if not sec or not isinstance(sec, str):
continue
ver = cap.get("version") or cap.get("obj", {}).get("version")
if ver and ver != 0:
real[str(int(ver))] = sec
if not real:
return False, "No secrets found."
versions = sorted(int(k) for k in real.keys())
secret_bytes = [
{"version": v, "secret": [ord(c) for c in real[str(v)]]}
for v in versions
]
secrets_dir = Path.home() / ".spotify-secret"
secrets_dir.mkdir(exist_ok=True)
output_file = secrets_dir / "secretBytes.json"
with open(output_file, "w") as f:
json.dump(secret_bytes, f, indent=2)
return True, f"Saved to: {output_file}"
def grab_live(progress_callback=None):
def emit_progress(msg):
if progress_callback:
progress_callback(msg)
else:
print(msg)
stealth = """(()=>{
Object.defineProperty(navigator,'webdriver',{get:()=>false});
Object.defineProperty(navigator,'languages',{get:()=>['en-US','en']});
Object.defineProperty(navigator,'plugins',{get:()=>[1,2,3,4,5]});
window.chrome={runtime:{}};
const q=navigator.permissions.query;
navigator.permissions.query=p=>p.name==='notifications'?Promise.resolve({state:Notification.permission}):q(p);
const g=WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter=function(p){
if(p===37445)return'Intel Inc.';if(p===37446)return'Intel Iris OpenGL Engine';return g.call(this,p);
};
})();"""
hook = """(()=>{if(globalThis.__secretHookInstalled)return;
globalThis.__secretHookInstalled=true;globalThis.__captures=[];
Object.defineProperty(Object.prototype,'secret',{configurable:true,set:function(v){
try{__captures.push({secret:v,version:this.version,obj:this});}catch(e){}
Object.defineProperty(this,'secret',{value:v,writable:true,configurable:true,enumerable:true});}});
})();"""
co = ChromiumOptions()
co.headless(True)
co.set_argument('--disable-blink-features=AutomationControlled')
co.set_argument('--no-sandbox')
page = ChromiumPage(addr_or_opts=co)
try:
page.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=stealth)
page.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=hook)
emit_progress("Opening Spotify...")
page.get("https://open.spotify.com")
time.sleep(3)
caps = page.run_js("return globalThis.__captures || []")
for c in caps:
if isinstance(c, dict) and c.get("secret") and c.get("version"):
emit_progress(f"Secret({int(c['version'])}): {c['secret']}")
return caps or []
finally:
page.quit()
def scrape_and_save(progress_callback=None):
try:
caps = grab_live(progress_callback)
return summarise(caps)
except Exception as e:
return False, f"Error: {str(e)}"
def main():
success, message = scrape_and_save()
print(message)
return 0 if success else 1
if __name__ == "__main__":
import sys
sys.exit(main())
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

+1
View File
@@ -6,3 +6,4 @@ mutagen
pyotp pyotp
packaging packaging
pyinstaller pyinstaller
DrissionPage
+17
View File
@@ -0,0 +1,17 @@
[
"vogel.qqdl.site",
"maus.qqdl.site",
"hund.qqdl.site",
"eu-maus.qqdl.site",
"eu-katze.qqdl.site",
"katze.qqdl.site",
"wolf.qqdl.site",
"zeus.squid.wtf",
"shiva.squid.wtf",
"kraken.squid.wtf",
"dev-api.squid.wtf",
"chaos.squid.wtf",
"phoenix.squid.wtf",
"triton.squid.wtf",
"aether.squid.wtf"
]
+104 -10
View File
@@ -1,9 +1,9 @@
import asyncio
import json
import os import os
import re import re
import time import time
import base64
import requests import requests
import json
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
from mutagen.id3 import PictureType from mutagen.id3 import PictureType
@@ -16,19 +16,75 @@ class ProgressCallback:
print(f"\r{current / (1024 * 1024):.2f} MB", end="") print(f"\r{current / (1024 * 1024):.2f} MB", end="")
class TidalDownloader: class TidalDownloader:
def __init__(self, timeout=30, max_retries=3): def __init__(self, timeout=30, max_retries=3, api_url=None):
self.timeout = timeout self.timeout = timeout
self.max_retries = max_retries self.max_retries = max_retries
self.download_chunk_size = 256 * 1024 self.download_chunk_size = 256 * 1024
self.progress_callback = ProgressCallback() self.progress_callback = ProgressCallback()
self.client_id = "zU4XHVVkc2tDPo4t" self.client_id = base64.b64decode("NkJEU1JkcEs5aHFFQlRnVQ==").decode()
self.client_secret = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=" self.client_secret = base64.b64decode("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=").decode()
self.api_url = api_url
@staticmethod
def get_available_apis():
try:
response = requests.get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/tidal.json", timeout=10)
if response.status_code == 200:
api_list = response.json()
api_instances = [{"url": f"https://{api}"} for api in api_list]
return api_instances
else:
print(f"Failed to fetch API list: HTTP {response.status_code}")
return []
except Exception as e:
print(f"Failed to fetch API list: {e}")
return []
@staticmethod
def select_api_interactive():
apis = TidalDownloader.get_available_apis()
if not apis:
raise Exception("No APIs available. Cannot proceed.")
print("\n=== Available API Instances ===")
print(f"{'No':<4} {'URL':<50}")
print("-" * 60)
for i, api in enumerate(apis, 1):
url = api.get('url', 'N/A')
print(f"{i:<4} {url:<50}")
print("-" * 60)
while True:
try:
choice = input(f"\nSelect API (1-{len(apis)}) [1]: ").strip()
if not choice:
choice = "1"
choice_num = int(choice)
if 1 <= choice_num <= len(apis):
selected_url = apis[choice_num - 1]['url']
print(f"\nSelected: {selected_url}")
return selected_url
else:
print(f"Invalid choice. Please enter 1-{len(apis)}")
except ValueError:
print("Invalid input. Please enter a number.")
except KeyboardInterrupt:
print("\nCancelled")
raise Exception("API selection cancelled")
def set_progress_callback(self, callback): def set_progress_callback(self, callback):
self.progress_callback = callback self.progress_callback = callback
def sanitize_filename(self, filename): def sanitize_filename(self, filename):
if not filename: if not filename:
return "Unknown Track" return "Unknown Track"
@@ -144,7 +200,7 @@ class TidalDownloader:
def get_download_url(self, track_id, quality="LOSSLESS"): def get_download_url(self, track_id, quality="LOSSLESS"):
print("Fetching URL...") print("Fetching URL...")
download_api_url = f"https://tidal.401658.xyz/track/?id={track_id}&quality={quality}" download_api_url = f"{self.api_url}/track/?id={track_id}&quality={quality}"
try: try:
response = requests.get(download_api_url, timeout=self.timeout) response = requests.get(download_api_url, timeout=self.timeout)
@@ -184,6 +240,10 @@ class TidalDownloader:
return None return None
def download_file(self, url, filepath, is_paused_callback=None, is_stopped_callback=None): def download_file(self, url, filepath, is_paused_callback=None, is_stopped_callback=None):
file_dir = os.path.dirname(filepath)
if file_dir and not os.path.exists(file_dir):
os.makedirs(file_dir, exist_ok=True)
temp_filepath = filepath + ".part" temp_filepath = filepath + ".part"
retry_count = 0 retry_count = 0
@@ -316,13 +376,45 @@ class TidalDownloader:
print(f"Error embedding metadata: {str(e)}") print(f"Error embedding metadata: {str(e)}")
return False return False
def download(self, query, isrc=None, output_dir=".", quality="LOSSLESS", is_paused_callback=None, is_stopped_callback=None): def download(self, query, isrc=None, output_dir=".", quality="LOSSLESS", is_paused_callback=None, is_stopped_callback=None, auto_fallback=False):
if output_dir != ".": if output_dir != ".":
try: try:
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
except OSError as e: except OSError as e:
raise Exception(f"Directory error: {e}") raise Exception(f"Directory error: {e}")
if auto_fallback:
apis = self.get_available_apis()
if not apis:
raise Exception("No APIs available for fallback")
last_error = None
for i, api in enumerate(apis, 1):
api_url = api.get('url')
try:
print(f"[Auto Fallback {i}/{len(apis)}] Trying: {api_url}")
fallback_downloader = TidalDownloader(api_url=api_url)
fallback_downloader.set_progress_callback(self.progress_callback)
result = fallback_downloader._download_single(
query, isrc, output_dir, quality,
is_paused_callback, is_stopped_callback
)
print(f"✓ Success with: {api_url}")
return result
except Exception as e:
last_error = str(e)
print(f"✗ Failed with {api_url}: {last_error[:80]}")
continue
raise Exception(f"All {len(apis)} APIs failed. Last error: {last_error}")
return self._download_single(query, isrc, output_dir, quality, is_paused_callback, is_stopped_callback)
def _download_single(self, query, isrc, output_dir, quality, is_paused_callback, is_stopped_callback):
track_info = self.get_track_info(query, isrc) track_info = self.get_track_info(query, isrc)
track_id = track_info.get("id") track_id = track_info.get("id")
@@ -373,7 +465,9 @@ class TidalDownloader:
def main(): def main():
print("=== TidalDL - Tidal Downloader ===") print("=== TidalDL - Tidal Downloader ===")
downloader = TidalDownloader(timeout=30, max_retries=3)
selected_api = TidalDownloader.select_api_interactive()
downloader = TidalDownloader(timeout=30, max_retries=3, api_url=selected_api)
query = "APT." query = "APT."
isrc = "USAT22409172" isrc = "USAT22409172"
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "4.7" "version": "5.3"
} }