Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 711c5a98d3 | |||
| fd949a17f0 | |||
| 1c0de8b3ac | |||
| b653a8ca41 | |||
| 2d0c174c50 | |||
| c63eeccc55 | |||
| a620c16b1c | |||
| cf27ae098d | |||
| a0c60a473a | |||
| 25c5a4d175 | |||
| 33a6137f75 | |||
| b4fcb6bca6 | |||
| 5ab19a6d37 | |||
| 8547e6d410 | |||
| 17666d8027 | |||
| ab208482ca | |||
| 76e02d77e8 | |||
| 75cc4543ad | |||
| 0b468c4b60 | |||
| 87a6a778f7 | |||
| ef893ab9f4 | |||
| 3eda3245ca | |||
| f6f238361c | |||
| 998730bbb3 | |||
| 56a1d29d78 | |||
| 4b7316636e |
@@ -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.6/SpotiFLAC.exe)
|
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/latest/download/SpotiFLAC.exe)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Lossless Audio Check
|
## Lossless Audio Check
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,20 +13,34 @@ from typing import Dict, Any, List, Tuple
|
|||||||
def get_random_user_agent():
|
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)}"
|
||||||
|
|
||||||
|
# https://github.com/xyloflake/spot-secrets-go
|
||||||
def generate_totp():
|
def generate_totp():
|
||||||
url = "https://raw.githubusercontent.com/Thereallo1026/spotify-secrets/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))
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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 |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -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 |
@@ -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 |
@@ -5,4 +5,5 @@ requests
|
|||||||
mutagen
|
mutagen
|
||||||
pyotp
|
pyotp
|
||||||
packaging
|
packaging
|
||||||
pyinstaller
|
pyinstaller
|
||||||
|
DrissionPage
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "4.6"
|
"version": "5.3"
|
||||||
}
|
}
|
||||||
|
|||||||