Created
May 18, 2025 11:26
-
-
Save imjyotiraditya/7146b47c09e1227b3c6adcee4d4b4f0e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# SPDX-FileCopyrightText: Jyotiraditya Panda <[email protected]> | |
# SPDX-License-Identifier: MIT | |
import argparse | |
import sys | |
import requests | |
BASE_URL = "https://zvuk.com" | |
API_ENDPOINTS = { | |
"lyrics": f"{BASE_URL}/api/tiny/lyrics", | |
"stream": f"{BASE_URL}/api/tiny/track/stream", | |
"graphql": f"{BASE_URL}/api/v1/graphql", | |
"profile": f"{BASE_URL}/api/tiny/profile", | |
} | |
TOKEN = "" | |
HEADERS = { | |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", | |
"Content-Type": "application/json", | |
} | |
def get_anonymous_token(): | |
try: | |
response = requests.get(API_ENDPOINTS["profile"], headers=HEADERS) | |
response.raise_for_status() | |
data = response.json() | |
if "result" in data and "token" in data["result"]: | |
return data["result"]["token"] | |
raise ValueError("Token not found in API response") | |
except Exception as e: | |
raise Exception(f"Failed to retrieve anonymous token: {e}") | |
def get_auth_cookies(): | |
global TOKEN | |
if not TOKEN: | |
TOKEN = get_anonymous_token() | |
return {"auth": TOKEN} | |
def get_track_lyrics(track_id): | |
params = {"track_id": track_id} | |
response = requests.get( | |
API_ENDPOINTS["lyrics"], | |
params=params, | |
headers=HEADERS, | |
cookies=get_auth_cookies(), | |
) | |
response.raise_for_status() | |
data = response.json() | |
if "result" in data and "lyrics" in data["result"]: | |
return data["result"]["lyrics"] | |
return "No lyrics found" | |
def get_stream_url(track_id): | |
params = {"id": track_id, "quality": "mid"} | |
response = requests.get( | |
API_ENDPOINTS["stream"], | |
params=params, | |
headers=HEADERS, | |
cookies=get_auth_cookies(), | |
) | |
response.raise_for_status() | |
data = response.json() | |
if "result" in data and "stream" in data["result"]: | |
return data["result"]["stream"] | |
raise ValueError("No stream URL found in API response") | |
def search_tracks(query): | |
graphql_query = """ | |
query getSearchTracks($query: String) { | |
search(query: $query) { | |
tracks(limit: 50) { | |
items { | |
id | |
title | |
duration | |
explicit | |
artists { | |
id | |
title | |
} | |
release { | |
id | |
title | |
} | |
} | |
} | |
} | |
} | |
""" | |
payload = { | |
"query": graphql_query, | |
"variables": {"query": query}, | |
"operationName": "getSearchTracks", | |
} | |
response = requests.post( | |
API_ENDPOINTS["graphql"], | |
json=payload, | |
headers=HEADERS, | |
cookies=get_auth_cookies(), | |
) | |
response.raise_for_status() | |
data = response.json() | |
if ( | |
"data" in data | |
and "search" in data["data"] | |
and "tracks" in data["data"]["search"] | |
): | |
return data["data"]["search"]["tracks"]["items"] | |
return [] | |
def format_duration(seconds): | |
minutes, seconds = divmod(seconds, 60) | |
return f"{minutes}:{seconds:02d}" | |
def stream_command(args): | |
try: | |
if not TOKEN: | |
sys.stderr.write( | |
"Error: Authentication token must be manually set for stream functionality.\n" | |
) | |
sys.stderr.write("To get a token:\n") | |
sys.stderr.write("1. Log in to Zvuk.com in your browser\n") | |
sys.stderr.write("2. Visit https://zvuk.com/api/v2/tiny/profile\n") | |
sys.stderr.write("3. Copy the token value from the response\n") | |
sys.stderr.write("4. Set the TOKEN variable in this script\n") | |
sys.exit(1) | |
stream_url = get_stream_url(args.track_id) | |
print(stream_url) | |
except Exception as e: | |
sys.stderr.write(f"Error: {e}\n") | |
sys.exit(1) | |
def lyrics_command(args): | |
try: | |
lyrics = get_track_lyrics(args.track_id) | |
print(lyrics) | |
except Exception as e: | |
sys.stderr.write(f"Error: {e}\n") | |
sys.exit(1) | |
def search_command(args): | |
try: | |
tracks = search_tracks(args.query) | |
if not tracks: | |
print("No tracks found") | |
return | |
print(f"Found {len(tracks)} tracks:") | |
for i, track in enumerate(tracks, 1): | |
artists = ", ".join([artist["title"] for artist in track["artists"]]) | |
duration = format_duration(track["duration"]) | |
print(f"{i}. {track['title']} - {artists} ({duration}) [ID: {track['id']}]") | |
except Exception as e: | |
sys.stderr.write(f"Error: {e}\n") | |
sys.exit(1) | |
def main(): | |
parser = argparse.ArgumentParser( | |
description="Zvuk.com API client for searching tracks and fetching lyrics and streams" | |
) | |
subparsers = parser.add_subparsers(dest="command", help="Command to run") | |
stream_parser = subparsers.add_parser("stream", help="Get stream URL for a track") | |
stream_parser.add_argument("track_id", help="Track ID to fetch stream for") | |
stream_parser.set_defaults(func=stream_command) | |
lyrics_parser = subparsers.add_parser("lyrics", help="Get lyrics for a track") | |
lyrics_parser.add_argument("track_id", help="Track ID to fetch lyrics for") | |
lyrics_parser.set_defaults(func=lyrics_command) | |
search_parser = subparsers.add_parser("search", help="Search for tracks") | |
search_parser.add_argument("query", help="Search query") | |
search_parser.set_defaults(func=search_command) | |
args = parser.parse_args() | |
if hasattr(args, "func"): | |
args.func(args) | |
else: | |
parser.print_help() | |
if __name__ == "__main__": | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment