Compare commits

...

54 Commits

Author SHA1 Message Date
afkarxyz 3651833e2a Update v2.2 2025-03-18 07:56:51 +07:00
afkarxyz 8403b96306 Update README.md 2025-03-18 07:55:16 +07:00
afkarxyz d977829e36 Update v2.1 2025-03-02 15:21:25 +07:00
afkarxyz 2aaf123c98 Update v2.1 2025-03-02 15:21:14 +07:00
afkarxyz 13567802a0 Update v2.1 2025-03-02 15:18:04 +07:00
afkarxyz d704782519 Update v2.0 2025-03-02 14:52:04 +07:00
afkarxyz 884c02278f Update v2.0 2025-03-02 14:51:46 +07:00
afkarxyz d6abe2bae3 Update v2.0 2025-03-02 14:48:15 +07:00
afkarxyz 7b858dd0ce Update v2.0 2025-03-02 14:47:55 +07:00
afkarxyz cfeb9a2ef2 Update v1.9 2025-03-02 14:05:17 +07:00
afkarxyz 81a78832ff Update v1.9 2025-03-02 14:02:16 +07:00
afkarxyz 071f20deff Update README.md 2025-03-02 14:01:46 +07:00
afkarxyz 03de68ac7b Rename to getMetadata.py 2025-03-02 06:45:20 +07:00
afkarxyz 77363f9e61 Update README.md 2025-03-02 06:33:53 +07:00
afkarxyz 2df77120cf Update v1.8 2025-03-02 06:30:12 +07:00
afkarxyz e6e953b2ed Update README.md 2025-03-02 06:18:19 +07:00
afkarxyz 72f17479e8 Update getTracks.py 2025-02-19 15:48:43 +07:00
afkarxyz 9286fba63c Update README.md 2025-02-19 09:42:23 +07:00
afkarxyz 6bf7084959 Update version.json 2025-02-19 09:42:11 +07:00
afkarxyz 03cc3d82a7 Update v1.7 2025-02-19 09:41:58 +07:00
afkarxyz 2acd6fcba1 Update v1.6 2025-02-18 09:46:44 +07:00
afkarxyz 85a5bb2321 Update README.md 2025-02-18 09:45:29 +07:00
afkarxyz 70a955f531 Update README.md 2025-02-18 09:10:34 +07:00
afkarxyz 71c8070ec0 Create version.json 2025-02-18 07:58:39 +07:00
afkarxyz 177bc06b79 Update README.md 2025-02-04 14:24:02 +07:00
afkarxyz 2aec9c0185 Update v1.5 2025-02-04 14:22:51 +07:00
afkarxyz a6a84cf869 Update v1.5 2025-02-04 14:22:32 +07:00
afkarxyz 3577574ad8 Update v1.5 2025-02-04 14:22:01 +07:00
afkarxyz 3696fc95a7 Update README.md 2025-02-04 14:21:20 +07:00
afkarxyz effa462810 Update README.md 2025-01-22 12:50:04 +07:00
afkarxyz 921faefecf Update README.md 2025-01-22 05:39:13 +07:00
afkarxyz a4168450d1 Update README.md 2025-01-22 05:35:54 +07:00
afkarxyz 7ba3efb75b Update README.md 2025-01-22 05:34:59 +07:00
afkarxyz d18ba28864 Update v1.4 2025-01-14 21:28:08 +07:00
afkarxyz f8da9ecfd2 Update README.md 2025-01-14 21:27:50 +07:00
afkarxyz 2588680846 Update README.md 2025-01-14 15:50:16 +07:00
afkarxyz f3366f0554 Update README.md 2025-01-11 05:14:42 +07:00
afkarxyz 2da2ea64ee Update LucidaDownloader.py 2025-01-10 05:10:43 +07:00
afkarxyz 7e52b8ab35 Update v1.3 2025-01-10 05:10:27 +07:00
afkarxyz 4b89a5e678 Update README.md 2025-01-10 05:08:58 +07:00
afkarxyz c0677b3cb7 Update README.md 2025-01-10 03:58:18 +07:00
afkarxyz 176e4566df Update README.md 2025-01-09 23:25:27 +07:00
afkarxyz f319d0dcbb Update README.md 2025-01-09 23:22:32 +07:00
afkarxyz 61c53655ff Update v1.2 2025-01-09 23:16:16 +07:00
afkarxyz 77bbc70c9b Update README.md 2025-01-09 23:15:32 +07:00
afkarxyz 360ba44dd5 Update GetMetadata.py 2025-01-09 19:53:50 +07:00
afkarxyz 9011335181 Update LucidaDownloader.py 2025-01-09 19:53:38 +07:00
afkarxyz 3d0f21bf57 Update v1.1 2025-01-09 19:53:26 +07:00
afkarxyz b6fa1ec6a5 Update README.md 2025-01-09 19:49:38 +07:00
afkarxyz 9bfe08cad0 Update README.md 2025-01-09 15:38:10 +07:00
afkarxyz fcc04bf3b8 Add files via upload 2025-01-09 11:43:18 +07:00
afkarxyz 97cfdda82d Update README.md 2025-01-09 11:42:34 +07:00
afkarxyz df92237012 Update LucidaDownloader.py 2025-01-09 10:06:42 +07:00
afkarxyz 7eef8779ef Update README.md 2025-01-09 09:55:16 +07:00
7 changed files with 1796 additions and 124 deletions
-117
View File
@@ -1,117 +0,0 @@
import requests
from tqdm import tqdm
import time
import os
import asyncio
from GetMetadata import main as get_metadata
class TrackDownloader:
def __init__(self):
self.client = requests.Session()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
async def get_track_info(self):
metadata = await get_metadata()
return metadata
def sanitize_filename(self, filename):
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '')
filename = ' '.join(filename.split())
filename = filename.replace(' ,', ',')
filename = filename.replace(',', ', ').replace(' ', ' ')
return filename.strip()
def download(self, metadata, output_dir):
track_url = metadata['url']
primary_token = metadata['token']
expiry = metadata['expiry']
print(f"Starting download for: {track_url}")
initial_request = {
"account": {"id": "auto", "type": "country"},
"compat": "false",
"downscale": "original",
"handoff": True,
"metadata": True,
"private": True,
"token": {
"expiry": expiry,
"primary": primary_token
},
"upload": {"enabled": False, "service": "pixeldrain"},
"url": track_url
}
response = self.client.post("https://lucida.to/api/load?url=/api/fetch/stream/v2",
json=initial_request,
headers=self.headers)
csrf_token = response.cookies.get('csrf_token')
if csrf_token:
self.headers['X-CSRF-Token'] = csrf_token
initial_response = response.json()
if not initial_response.get("success", False):
raise Exception(f"Initial request failed: {initial_response.get('error', 'Unknown error')}")
handoff = initial_response["handoff"]
server = initial_response["server"]
file_name = f"{metadata['title']} - {metadata['artists']}.flac"
file_name = self.sanitize_filename(file_name)
completion_url = f"https://{server}.lucida.to/api/fetch/request/{handoff}"
print("Waiting for track processing to complete")
while True:
completion_response = self.client.get(completion_url, headers=self.headers).json()
if completion_response["status"] == "completed":
break
elif completion_response["status"] == "error":
raise Exception(f"API request failed: {completion_response.get('message', 'Unknown error')}")
time.sleep(1)
download_url = f"https://{server}.lucida.to/api/fetch/request/{handoff}/download"
print(f"Starting download of: {file_name}")
response = self.client.get(download_url, stream=True, headers=self.headers)
total_size = int(response.headers.get('content-length', 0))
file_path = os.path.join(output_dir, file_name)
with open(file_path, 'wb') as file, tqdm(
desc=file_name,
total=total_size,
unit='iB',
unit_scale=True,
unit_divisor=1024,
) as progress_bar:
for data in response.iter_content(chunk_size=1024):
size = file.write(data)
progress_bar.update(size)
print(f"Download completed: {file_path}")
return file_path
async def main():
downloader = TrackDownloader()
output_dir = "."
try:
metadata = await downloader.get_track_info()
downloaded_file = downloader.download(metadata, output_dir)
print(f"File downloaded successfully: {downloaded_file}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,7 +1,7 @@
import asyncio
import zendriver as zd
async def get_metadata(page):
async def get_metadata(page, headless=True):
max_attempts = 40
attempts = 0
@@ -14,9 +14,9 @@ async def get_metadata(page):
const [url, config] = args;
if (url.includes('/api/load?url=%2Fapi%2Ffetch%2Fstream%2Fv2')) {
const payload = JSON.parse(config.body);
const title = document.querySelector('h1.svelte-6pt9ji').textContent;
const title = document.querySelector('h1.svelte-6pt9ji').textContent.trim();
const artists = Array.from(document.querySelectorAll('h2.svelte-6pt9ji a.normal'))
.map(a => a.textContent)
.map(a => a.textContent.trim())
.join(', ');
const cover = document.querySelector('.svelte-6pt9ji .meta.svelte-6pt9ji a').href;
@@ -82,8 +82,8 @@ async def get_metadata(page):
raise TimeoutError("Timeout")
async def main():
browser = await zd.start(headless=False)
async def main(headless=True):
browser = await zd.start(headless=headless)
try:
track_id = "2plbrEY59IikOBgBGLjaoe"
url = f"https://lucida.to/?url=https%3A%2F%2Fopen.spotify.com%2Ftrack%2F{track_id}&country=auto&to=tidal"
+30 -2
View File
@@ -1,6 +1,34 @@
[![GitHub All Releases](https://img.shields.io/github/downloads/afkarxyz/SpotiFLAC/total?style=for-the-badge)](https://github.com/afkarxyz/SpotiFLAC/releases)
![spotiflac](https://github.com/user-attachments/assets/a233a276-14a4-4f4c-b267-f182dd3912a0)
<div align="center">
<b>SpotiFLAC</b> allows you to download Spotify tracks in true FLAC format through services like Tidal, Amazon Music and Deezer with the help of Lucida.
</div>
### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v2.2/SpotiFLAC.exe)
#
> [!WARNING]
Sometimes, the **download speed** from Lucida can be fast or slow; it varies unpredictably.
## Screenshots
![image](https://github.com/user-attachments/assets/649b85e8-d96f-4c80-a652-177b26cf621c)
![image](https://github.com/user-attachments/assets/7fa82a25-0fe8-4b87-ba5c-2dd5933f211b)
![image](https://github.com/user-attachments/assets/7ffd0367-83d6-4136-8a45-bb35c547a8c6)
![image](https://github.com/user-attachments/assets/81e65977-11f0-4162-96f3-90730dd87e74)
![image](https://github.com/user-attachments/assets/4dd37c0a-30e3-479a-9b3d-57fd360d87b3)
![image](https://github.com/user-attachments/assets/04954db9-e94a-4f9d-8eac-46d7ff7a4c33)
> When **Fallback** is enabled, it will use the backup server `Lucida.su`
## Lossless Audio Check
![image](https://github.com/user-attachments/assets/d63b422d-0ea3-4307-850f-96c99d7eaa9a)
![image](https://github.com/user-attachments/assets/7649e6e1-d5d1-49b3-b83f-965d44651d05)
#### [Download](https://github.com/afkarxyz/SpotiFLAC/releases/download/v0/FLAC-Checker.zip) FLAC Checker
+1127
View File
File diff suppressed because it is too large Load Diff
+319
View File
@@ -0,0 +1,319 @@
from time import sleep
from urllib.parse import urlparse, parse_qs
import requests
import json
import hmac
import time
import hashlib
from typing import Tuple, Callable
_TOTP_SECRET = bytearray([53,53,48,55,49,52,53,56,53,51,52,56,55,52,57,57,53,57,50,50,52,56,54,51,48,51,50,57,51,52,55])
def generate_totp(
secret: bytes = _TOTP_SECRET,
algorithm: Callable[[], object] = hashlib.sha1,
digits: int = 6,
counter_factory: Callable[[], int] = lambda: int(time.time()) // 30,
) -> Tuple[str, int]:
counter = counter_factory()
hmac_result = hmac.new(
secret, counter.to_bytes(8, byteorder="big"), algorithm
).digest()
offset = hmac_result[-1] & 15
truncated_value = (
(hmac_result[offset] & 127) << 24
| (hmac_result[offset + 1] & 255) << 16
| (hmac_result[offset + 2] & 255) << 8
| (hmac_result[offset + 3] & 255)
)
return (
str(truncated_value % (10**digits)).zfill(digits),
counter * 30_000,
)
token_url = 'https://open.spotify.com/get_access_token'
playlist_base_url = 'https://api.spotify.com/v1/playlists/{}'
album_base_url = 'https://api.spotify.com/v1/albums/{}'
track_base_url = 'https://api.spotify.com/v1/tracks/{}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'Referer': 'https://open.spotify.com/',
'Origin': 'https://open.spotify.com'
}
class SpotifyInvalidUrlException(Exception):
pass
class SpotifyWebsiteParserException(Exception):
pass
def parse_uri(uri):
u = urlparse(uri)
if u.netloc == "embed.spotify.com":
if not u.query:
raise SpotifyInvalidUrlException("ERROR: url {} is not supported".format(uri))
qs = parse_qs(u.query)
return parse_uri(qs['uri'][0])
if not u.scheme and not u.netloc:
return {"type": "playlist", "id": u.path}
if u.scheme == "spotify":
parts = uri.split(":")
else:
if u.netloc != "open.spotify.com" and u.netloc != "play.spotify.com":
raise SpotifyInvalidUrlException("ERROR: url {} is not supported".format(uri))
parts = u.path.split("/")
if parts[1] == "embed":
parts = parts[1:]
l = len(parts)
if l == 3 and parts[1] in ["album", "track", "playlist"]:
return {"type": parts[1], "id": parts[2]}
if l == 5 and parts[3] == "playlist":
return {"type": parts[3], "id": parts[4]}
raise SpotifyInvalidUrlException("ERROR: unable to determine Spotify URL type or type is unsupported.")
def get_json_from_api(api_url, access_token):
headers.update({'Authorization': 'Bearer {}'.format(access_token)})
req = requests.get(api_url, headers=headers, timeout=10)
if req.status_code == 429:
seconds = int(req.headers.get("Retry-After", "5")) + 1
print(f"INFO: rate limited! Sleeping for {seconds} seconds")
sleep(seconds)
return None
if req.status_code != 200:
raise SpotifyWebsiteParserException(f"ERROR: {api_url} gave us not a 200. Instead: {req.status_code}")
return req.json()
def get_raw_spotify_data(spotify_url):
url_info = parse_uri(spotify_url)
try:
totp, timestamp = generate_totp()
params = {
"reason": "init",
"productType": "web-player",
"totp": totp,
"totpVer": 5,
"ts": timestamp,
}
req = requests.get(token_url, headers=headers, params=params, timeout=10)
if req.status_code != 200:
return {"error": f"Failed to get access token. Status code: {req.status_code}"}
token = req.json()
except Exception as e:
return {"error": f"Failed to get access token: {str(e)}"}
raw_data = {}
if url_info['type'] == "playlist":
try:
playlist_data = get_json_from_api(
playlist_base_url.format(url_info["id"]),
token["accessToken"]
)
if not playlist_data:
return {"error": "Failed to get playlist data"}
raw_data = playlist_data
tracks = []
tracks_url = f'https://api.spotify.com/v1/playlists/{url_info["id"]}/tracks?limit=100'
while tracks_url:
track_data = get_json_from_api(tracks_url, token["accessToken"])
if not track_data:
break
tracks.extend(track_data['items'])
tracks_url = track_data.get('next')
raw_data['tracks']['items'] = tracks
except Exception as e:
return {"error": f"Failed to get playlist data: {str(e)}"}
elif url_info["type"] == "album":
try:
album_data = get_json_from_api(
album_base_url.format(url_info["id"]),
token["accessToken"]
)
if not album_data:
return {"error": "Failed to get album data"}
album_data['_token'] = token["accessToken"]
raw_data = album_data
tracks = []
tracks_url = f'{album_base_url.format(url_info["id"])}/tracks?limit=50'
while tracks_url:
track_data = get_json_from_api(tracks_url, token["accessToken"])
if not track_data:
break
tracks.extend(track_data['items'])
tracks_url = track_data.get('next')
raw_data['tracks']['items'] = tracks
except Exception as e:
return {"error": f"Failed to get album data: {str(e)}"}
elif url_info["type"] == "track":
try:
track_data = get_json_from_api(
track_base_url.format(url_info["id"]),
token["accessToken"]
)
if not track_data:
return {"error": "Failed to get track data"}
raw_data = track_data
except Exception as e:
return {"error": f"Failed to get track data: {str(e)}"}
return raw_data
def format_track_data(track_data):
artists = []
for artist in track_data['artists']:
artists.append(artist['name'])
image_url = track_data.get('album', {}).get('images', [{}])[0].get('url', '')
return {
"track": {
"artists": ", ".join(artists),
"name": track_data.get('name', ''),
"album_name": track_data.get('album', {}).get('name', ''),
"duration_ms": track_data.get('duration_ms', 0),
"images": image_url,
"release_date": track_data.get('album', {}).get('release_date', ''),
"track_number": track_data.get('track_number', 0),
"external_urls": track_data.get('external_urls', {}).get('spotify', '')
}
}
def format_album_data(album_data):
artists = []
for artist in album_data['artists']:
artists.append(artist['name'])
image_url = album_data.get('images', [{}])[0].get('url', '')
track_list = []
for track in album_data.get('tracks', {}).get('items', []):
track_artists = []
for artist in track.get('artists', []):
track_artists.append(artist['name'])
track_list.append({
"artists": ", ".join(track_artists),
"name": track.get('name', ''),
"album_name": album_data.get('name', ''),
"duration_ms": track.get('duration_ms', 0),
"images": image_url,
"release_date": album_data.get('release_date', ''),
"track_number": track.get('track_number', 0),
"external_urls": track.get('external_urls', {}).get('spotify', '')
})
return {
"album_info": {
"total_tracks": album_data.get('total_tracks', 0),
"name": album_data.get('name', ''),
"release_date": album_data.get('release_date', ''),
"artists": ", ".join(artists),
"images": image_url
},
"track_list": track_list
}
def format_playlist_data(playlist_data):
image_url = playlist_data.get('images', [{}])[0].get('url', '')
track_list = []
for item in playlist_data.get('tracks', {}).get('items', []):
track = item.get('track', {})
artists = []
for artist in track.get('artists', []):
artists.append(artist['name'])
track_image = track.get('album', {}).get('images', [{}])[0].get('url', '')
track_list.append({
"artists": ", ".join(artists),
"name": track.get('name', ''),
"album_name": track.get('album', {}).get('name', ''),
"duration_ms": track.get('duration_ms', 0),
"images": track_image,
"release_date": track.get('album', {}).get('release_date', ''),
"track_number": track.get('track_number', 0),
"external_urls": track.get('external_urls', {}).get('spotify', '')
})
return {
"playlist_info": {
"tracks": {"total": playlist_data.get('tracks', {}).get('total', 0)},
"followers": {"total": playlist_data.get('followers', {}).get('total', 0)},
"owner": {
"display_name": playlist_data.get('owner', {}).get('display_name', ''),
"name": playlist_data.get('name', ''),
"images": image_url
}
},
"track_list": track_list
}
def process_spotify_data(raw_data, data_type):
if not raw_data or "error" in raw_data:
return {"error": "Invalid data provided"}
try:
if data_type == "track":
return format_track_data(raw_data)
elif data_type == "album":
return format_album_data(raw_data)
elif data_type == "playlist":
return format_playlist_data(raw_data)
else:
return {"error": "Invalid data type"}
except Exception as e:
return {"error": f"Error processing data: {str(e)}"}
def get_filtered_data(spotify_url):
raw_data = get_raw_spotify_data(spotify_url)
if raw_data and "error" not in raw_data:
url_info = parse_uri(spotify_url)
filtered_data = process_spotify_data(raw_data, url_info['type'])
return filtered_data
return {"error": "Failed to get raw data"}
if __name__ == '__main__':
playlist = "https://open.spotify.com/playlist/37i9dQZEVXbNG2KDcFcKOF"
album = "https://open.spotify.com/album/7kFyd5oyJdVX2pIi6P4iHE"
song = "https://open.spotify.com/track/4wJ5Qq0jBN4ajy7ouZIV1c"
filtered_playlist = get_filtered_data(playlist)
print(json.dumps(filtered_playlist, indent=2))
filtered_album = get_filtered_data(album)
print(json.dumps(filtered_album, indent=2))
filtered_track = get_filtered_data(song)
print(json.dumps(filtered_track, indent=2))
+312
View File
@@ -0,0 +1,312 @@
import requests
import time
import os
import asyncio
import re
import base64
class TrackDownloader:
def __init__(self, use_fallback=False):
self.client = requests.Session()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self.progress_callback = None
self.filename_format = 'title_artist'
self.use_fallback = use_fallback
self.base_domain = "lucida.su" if use_fallback else "lucida.to"
def set_progress_callback(self, callback):
self.progress_callback = callback
def set_filename_format(self, format_type):
self.filename_format = format_type
def generate_filename(self, metadata):
if self.filename_format == 'artist_title':
filename = f"{metadata['artists']} - {metadata['title']}.flac"
else:
filename = f"{metadata['title']} - {metadata['artists']}.flac"
return self.sanitize_filename(filename)
async def get_track_info(self, track_id, service="amazon", use_fallback=None):
if use_fallback is None:
use_fallback = self.use_fallback
domain_type = "su" if use_fallback else "to"
spotify_url = f"https://open.spotify.com/track/{track_id}"
result = self.convert_spotify_link(spotify_url, service, domain_type)
if "error" in result:
raise Exception(f"Failed to get track info: {result['error']}")
return result
def convert_spotify_link(self, spotify_url, target_service="amazon", domain_type="to"):
track_id_match = re.search(r'track/([a-zA-Z0-9]+)', spotify_url)
if not track_id_match:
return {"error": "Invalid Spotify URL"}
domain = "lucida.to" if domain_type == "to" else "lucida.su"
base_url = f"https://{domain}"
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "id-ID,id;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Host": domain,
"Pragma": "no-cache",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
try:
headers["Referer"] = f"{base_url}/?url={spotify_url}&country=auto"
request_params = {
"url": spotify_url,
"country": "auto",
"to": target_service
}
session = requests.Session()
session.verify = False
response = session.get(
base_url,
params=request_params,
headers=headers,
timeout=30
)
html_content = response.text
token_match = re.search(r'token:"([^"]+)"', html_content)
token_expiry_match = re.search(r'tokenExpiry:(\d+)', html_content)
token = token_match.group(1) if token_match else None
token_expiry = int(token_expiry_match.group(1)) if token_expiry_match else None
url = None
url_patterns = [
r'"url":"([^"]+)"',
r'href="(https?://[^"]*' + re.escape(target_service) + r'[^"]*track[^"]*)"',
]
for pattern in url_patterns:
url_match = re.search(pattern, html_content)
if url_match:
url = url_match.group(1).replace('\\/', '/')
break
if not url:
redirect_patterns = [
r'url=([^&"]+)',
r'href="([^"]+)"',
r'window\.location\.href\s*=\s*[\'"]([^\'"]+)[\'"]',
]
for pattern in redirect_patterns:
matches = re.finditer(pattern, html_content)
for match in matches:
potential_url = match.group(1)
if potential_url.startswith('http') and target_service.lower() in potential_url.lower():
url = potential_url.replace('\\/', '/')
break
if not url:
service_urls = re.finditer(r'(https?://[^"\s]+' + re.escape(target_service) + r'[^"\s]+)', html_content)
for match in service_urls:
url = match.group(1).replace('\\/', '/')
break
result = {
"service": target_service,
"url": url,
"token": {
"primary": None,
"expiry": None
},
"title": "Title",
"artists": "Artist"
}
if token:
try:
decoded_once = base64.b64decode(token).decode('latin1')
decoded_token = base64.b64decode(decoded_once).decode('latin1')
result["token"]["primary"] = decoded_token
except Exception:
result["token"]["primary"] = token
result["token"]["expiry"] = token_expiry
return result
except Exception as error:
return {"error": str(error)}
def sanitize_filename(self, filename):
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '')
filename = ' '.join(filename.split())
filename = filename.replace(' ,', ',')
filename = filename.replace(',', ', ')
while ' ' in filename:
filename = filename.replace(' ', ' ')
filename = filename.rsplit('.', 1)
filename[0] = filename[0].strip()
return '.'.join(filename)
def download(self, metadata, output_dir):
track_url = metadata['url']
primary_token = metadata['token']['primary']
expiry = metadata['token']['expiry']
print(f"Starting download for: {track_url}")
initial_request = {
"account": {"id": "auto", "type": "country"},
"compat": "false",
"downscale": "original",
"handoff": True,
"metadata": True,
"private": True,
"token": {
"expiry": expiry,
"primary": primary_token
},
"upload": {"enabled": False, "service": "pixeldrain"},
"url": track_url
}
response = self.client.post(f"https://{self.base_domain}/api/load?url=/api/fetch/stream/v2",
json=initial_request,
headers=self.headers)
csrf_token = response.cookies.get('csrf_token')
if csrf_token:
self.headers['X-CSRF-Token'] = csrf_token
initial_response = response.json()
if not initial_response.get("success", False):
raise Exception(f"Initial request failed: {initial_response.get('error', 'Unknown error')}")
handoff = initial_response["handoff"]
server = initial_response["server"]
file_name = self.generate_filename(metadata)
completion_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}"
print("Waiting for track processing to complete")
while True:
completion_response = self.client.get(completion_url, headers=self.headers).json()
status = completion_response["status"]
if status == "completed":
print("Processing completed: 100%")
break
elif status == "error":
raise Exception(f"API request failed: {completion_response.get('message', 'Unknown error')}")
else:
progress = completion_response.get("progress", {})
if progress:
current = progress.get("current", 0)
total = progress.get("total", 100)
percent = int((current / total) * 100) if total > 0 else 0
action = progress.get("action", "Processing")
print(f"Progress: {percent}% - {action} ({current}/{total})")
if action.lower() == "metadata":
if self.progress_callback:
self.progress_callback(0, 0)
else:
print(f"Status: {status} - Waiting for progress information...")
if status.lower() == "metadata":
if self.progress_callback:
self.progress_callback(0, 0)
time.sleep(1)
download_url = f"https://{server}.{self.base_domain}/api/fetch/request/{handoff}/download"
print(f"Starting download of: {file_name}")
response = self.client.get(download_url, stream=True, headers=self.headers)
total_size = int(response.headers.get('content-length', 0))
downloaded_size = 0
file_path = os.path.join(output_dir, file_name)
try:
with open(file_path, 'wb') as file:
start_time = time.time()
last_update_time = start_time
for chunk in response.iter_content(chunk_size=8192):
if chunk:
file.write(chunk)
downloaded_size += len(chunk)
current_time = time.time()
if current_time - last_update_time >= 1:
if total_size > 0:
progress_percent = (downloaded_size / total_size) * 100
elapsed_time = current_time - start_time
speed = downloaded_size / (1024 * 1024 * elapsed_time) if elapsed_time > 0 else 0
print(f"Download progress: {progress_percent:.2f}% ({downloaded_size}/{total_size}) - {speed:.2f} MB/s")
else:
print(f"Downloaded {downloaded_size / (1024 * 1024):.2f} MB")
last_update_time = current_time
if self.progress_callback:
self.progress_callback(downloaded_size, total_size)
if downloaded_size == 0:
raise Exception("No data received from server")
print(f"Download completed: {file_path}")
return file_path
except Exception as e:
if os.path.exists(file_path) and os.path.getsize(file_path) == 0:
try:
os.remove(file_path)
except:
pass
raise e
async def main():
use_fallback = False
downloader = TrackDownloader(use_fallback)
output_dir = "."
track_id = "2plbrEY59IikOBgBGLjaoe"
service = "amazon"
def progress_update(current, total):
if total > 0:
percent = (current / total) * 100
print(f"\rDownload progress: {percent:.2f}% ({current}/{total})", end="")
downloader.set_progress_callback(progress_update)
try:
print(f"Getting track info for ID: {track_id} from {service}")
metadata = await downloader.get_track_info(track_id, service)
print(f"Track info received, starting download process")
downloaded_file = downloader.download(metadata, output_dir)
print(f"\nFile downloaded successfully: {downloaded_file}")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
asyncio.run(main())
+3
View File
@@ -0,0 +1,3 @@
{
"version": "2.1"
}