Spaces:
Running
Running
import base64 | |
import logging | |
from typing import Optional, Dict | |
from fastapi import FastAPI, HTTPException, Request | |
import requests | |
from bs4 import BeautifulSoup | |
import os | |
from datetime import datetime, timedelta | |
import time | |
from requests.adapters import HTTPAdapter | |
from urllib3.util.retry import Retry | |
import asyncio | |
from typing import Optional, Dict, Tuple | |
import urllib.parse | |
from fastapi.responses import JSONResponse | |
import re | |
from ytmusicapi import YTMusic | |
# Configure logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.StreamHandler(), | |
logging.FileHandler('spotify_api.log') | |
] | |
) | |
logger = logging.getLogger(__name__) | |
app = FastAPI(title="Spotify Track API", | |
description="API for retrieving Spotify track information and download URLs") | |
# Constants | |
SPOTIFY_API_URL = "https://api.spotify.com/v1" | |
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") | |
SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") | |
TOKEN_EXPIRY = 3500 # Slightly less than 1 hour to ensure token refresh before expiration | |
# Token cache | |
class TokenCache: | |
def __init__(self): | |
self.token: Optional[str] = None | |
self.expiry_time: Optional[datetime] = None | |
def set_token(self, token: str): | |
self.token = token | |
self.expiry_time = datetime.now() + timedelta(seconds=TOKEN_EXPIRY) | |
def get_token(self) -> Optional[str]: | |
if not self.token or not self.expiry_time or datetime.now() >= self.expiry_time: | |
return None | |
return self.token | |
def is_expired(self) -> bool: | |
return not self.token or not self.expiry_time or datetime.now() >= self.expiry_time | |
token_cache = TokenCache() | |
# Custom exception for Spotify API errors | |
class SpotifyAPIError(Exception): | |
pass | |
def get_spotify_token() -> str: | |
""" | |
Get Spotify access token with expiration handling. | |
Returns a valid token, either from cache or by requesting a new one. | |
""" | |
try: | |
# Check if we have a valid cached token | |
cached_token = token_cache.get_token() | |
if cached_token: | |
logger.info("Using cached Spotify token") | |
return cached_token | |
logger.info("Requesting new Spotify access token") | |
start_time = time.time() | |
if not SPOTIFY_CLIENT_ID or not SPOTIFY_CLIENT_SECRET: | |
raise SpotifyAPIError("Spotify credentials not configured") | |
auth_string = f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}" | |
auth_bytes = base64.b64encode(auth_string.encode()).decode() | |
auth_response = requests.post( | |
'https://accounts.spotify.com/api/token', | |
data={'grant_type': 'client_credentials'}, | |
headers={'Authorization': f'Basic {auth_bytes}'}, | |
timeout=10 | |
) | |
if auth_response.status_code != 200: | |
raise SpotifyAPIError(f"Failed to get token: {auth_response.text}") | |
new_token = auth_response.json()['access_token'] | |
token_cache.set_token(new_token) | |
logger.info(f"New token obtained successfully in {time.time() - start_time:.2f}s") | |
return new_token | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Network error during token request: {str(e)}") | |
raise HTTPException(status_code=503, detail="Spotify authentication service unavailable") | |
except Exception as e: | |
logger.error(f"Unexpected error during token request: {str(e)}") | |
raise HTTPException(status_code=500, detail="Internal server error") | |
async def get_download_url(url): | |
loop = asyncio.get_running_loop() | |
url = "https://chrunos-ytdl2.hf.space/download" | |
data = {"url": url} | |
try: | |
response = await loop.run_in_executor(None, requests.post, url, {'json': data}) | |
if response.status_code == 200: | |
result = response.json() | |
return result.get('download_url') | |
else: | |
logger.error(f"请求失败,状态码: {response.status_code}") | |
return None | |
except requests.RequestException as e: | |
logger.error(f"发生客户端错误: {e}") | |
return None | |
def extract_album_id(album_url: str) -> str: | |
"""Extract album ID from Spotify URL.""" | |
try: | |
return album_url.split("/")[-1].split("?")[0] | |
except Exception as e: | |
logger.error(f"Failed to extract album ID from URL {album_url}: {str(e)}") | |
raise HTTPException(status_code=400, detail="Invalid Spotify album URL format") | |
async def fetch_spotify_track_info(track_id: str) -> Dict: | |
""" | |
Asynchronously fetch Spotify track title, artist, and cover art URL. | |
""" | |
token = get_spotify_token() | |
headers = { | |
'Authorization': f'Bearer {token}' | |
} | |
url = f"{SPOTIFY_API_URL}/tracks/{track_id}" | |
try: | |
loop = asyncio.get_running_loop() | |
response = await loop.run_in_executor(None, requests.get, url, {'headers': headers}) | |
if response.status_code == 200: | |
track_data = response.json() | |
title = track_data.get('name') | |
artists = [artist.get('name') for artist in track_data.get('artists', [])] | |
cover_art_url = track_data.get('album', {}).get('images', [{}])[0].get('url') | |
return { | |
'title': title, | |
'artists': artists, | |
'cover_art_url': cover_art_url | |
} | |
else: | |
raise SpotifyAPIError(f"Failed to fetch track information: {response.text}") | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Network error during track information request: {str(e)}") | |
raise HTTPException(status_code=503, detail="Spotify API service unavailable") | |
except Exception as e: | |
logger.error(f"Unexpected error during track information request: {str(e)}") | |
raise HTTPException(status_code=500, detail="Internal server error") | |
async def get_album_data(request: Request): | |
try: | |
# Get the JSON data from the request | |
data = await request.json() | |
album_url = data.get('album_url') | |
if not album_url: | |
raise HTTPException(status_code=400, detail="Missing 'album_url' in JSON data") | |
# Extract the album ID from the URL | |
album_id = extract_album_id(album_url) | |
# Get the Spotify access token | |
access_token = get_spotify_token() | |
# Make a request to the Spotify API to get album data | |
headers = { | |
'Authorization': f'Bearer {access_token}' | |
} | |
album_api_url = f"{SPOTIFY_API_URL}/albums/{album_id}" | |
response = requests.get(album_api_url, headers=headers, timeout=10) | |
if response.status_code != 200: | |
raise SpotifyAPIError(f"Failed to get album data: {response.text}") | |
album_data = response.json() | |
return album_data | |
except SpotifyAPIError as e: | |
logger.error(f"Spotify API error: {str(e)}") | |
raise HTTPException(status_code=500, detail=str(e)) | |
except Exception as e: | |
logger.error(f"Unexpected error: {str(e)}") | |
raise HTTPException(status_code=500, detail="Internal server error") | |
def extract_playlist_id(playlist_url: str) -> str: | |
"""Extract playlist ID from Spotify URL.""" | |
try: | |
return playlist_url.split("/")[-1].split("?")[0] | |
except Exception as e: | |
logger.error(f"Failed to extract playlist ID from URL {playlist_url}: {str(e)}") | |
raise HTTPException(status_code=400, detail="Invalid Spotify playlist URL format") | |
async def get_playlist_data(request: Request): | |
try: | |
# Get the JSON data from the request | |
data = await request.json() | |
playlist_url = data.get('playlist_url') | |
if not playlist_url: | |
raise HTTPException(status_code=400, detail="Missing 'playlist_url' in JSON data") | |
# Extract the playlist ID from the URL | |
playlist_id = extract_playlist_id(playlist_url) | |
logger.info(f"Extracted playlist ID: {playlist_id}") | |
# Get the Spotify access token | |
access_token = get_spotify_token() | |
# Make a request to the Spotify API to get playlist data | |
headers = { | |
'Authorization': f'Bearer {access_token}' | |
} | |
playlist_api_url = f"{SPOTIFY_API_URL}/playlists/{playlist_id}/tracks" | |
response = requests.get(playlist_api_url, headers=headers, timeout=10) | |
if response.status_code != 200: | |
raise SpotifyAPIError(f"Failed to get playlist data: {response.text}") | |
playlist_data = response.json() | |
return playlist_data | |
except SpotifyAPIError as e: | |
logger.error(f"Spotify API error: {str(e)}") | |
raise HTTPException(status_code=500, detail=str(e)) | |
except Exception as e: | |
logger.error(f"Unexpected error: {str(e)}") | |
raise HTTPException(status_code=500, detail="Internal server error") | |
def extract_track_id(track_url: str) -> str: | |
"""Extract track ID from Spotify URL.""" | |
try: | |
return track_url.split("/")[-1].split("?")[0] | |
except Exception as e: | |
logger.error(f"Failed to extract track ID from URL {track_url}: {str(e)}") | |
raise HTTPException(status_code=400, detail="Invalid Spotify URL format") | |
def get_cookie(): | |
headers = { | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' | |
} | |
try: | |
session = requests.Session() | |
response = session.get('https://spotisongdownloader.to/', headers=headers) | |
response.raise_for_status() | |
cookies = session.cookies.get_dict() | |
return f"PHPSESSID={cookies['PHPSESSID']}; quality=m4a" | |
except requests.exceptions.RequestException: | |
return None | |
def get_api(): | |
headers = { | |
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' | |
} | |
try: | |
response = requests.get('https://spotisongdownloader.to/track.php', headers=headers) | |
response.raise_for_status() | |
match = re.search(r'url:\s*"(/api/composer/spotify/[^"]+)"', response.text) | |
if match: | |
api_endpoint = match.group(1) | |
return f"https://spotisongdownloader.to{api_endpoint}" | |
except requests.exceptions.RequestException: | |
return None | |
def get_data(track_id): | |
link = f"https://open.spotify.com/track/{track_id}" | |
try: | |
response = requests.get( | |
'https://spotisongdownloader.to/api/composer/spotify/xsingle_track.php', | |
params={'url': link} | |
) | |
return response.json() | |
except: | |
return None | |
def get_url(track_data, cookie): | |
url = get_api() | |
if not url: | |
return None | |
payload = { | |
'song_name': track_data['song_name'], | |
'artist_name': track_data['artist'], | |
'url': track_data['url'] | |
} | |
headers = { | |
'Accept': 'application/json, text/javascript, */*; q=0.01', | |
'Cookie': cookie, | |
'Origin': 'https://spotisongdownloader.to', | |
'Referer': 'https://spotisongdownloader.to/track.php', | |
'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' | |
} | |
try: | |
response = requests.post(url, data=payload, headers=headers) | |
response.raise_for_status() | |
download_data = response.json() | |
encoded_link = urllib.parse.quote(download_data['dlink'], safe=':/?=') | |
return encoded_link | |
except: | |
return None | |
''' | |
@app.get("/{track_id}") | |
async def download_track(track_id: str): | |
cookie = get_cookie() | |
if not cookie: | |
return {"error": "Failed to get session cookie"}, 500 | |
track_data = get_data(track_id) | |
if not track_data: | |
return {"error": "Failed to get track data"}, 404 | |
download_link = get_url(track_data, cookie) | |
if not download_link: | |
return {"error": "Failed to get download URL"}, 500 | |
return {"url": download_link} | |
''' | |
async def download_track(track_id: str): | |
url = f'https://open.spotify.com/track/{track_id}' | |
track_data = get_song_link_info(url) | |
if not track_data: | |
track_data = fetch_spotify_track_info(track_id) | |
title = track_data["title"] | |
artist = track_data["artist"] | |
query = f'{title}+{artist}' | |
logger.info(f"search query: {query}") | |
search_results = ytmusic.search(query, filter="songs") | |
first_song = next((song for song in search_results if 'videoId' in song and song['videoId']), {}) if search_results else {} | |
if 'videoId' in first_song: | |
videoId = first_song["videoId"] | |
ym_url = f'https://www.youtube.com/watch?v={videoId}' | |
d_data = get_download_url(ym_url) | |
track_data['download_url'] = d_data | |
return track_data | |
else: | |
yt_url = track_data['url'] | |
d_data = get_download_url(yt_url) | |
track_data['download_url'] = d_data | |
return track_data | |
download_link = get_url(track_data, cookie) | |
if not download_link: | |
return {"error": "Failed to get download URL"}, 500 | |
return {"url": download_link} | |
# Function to get track info from Song.link API | |
def get_song_link_info(url: str): | |
# Check if the URL is from Amazon Music | |
if "music.amazon.com" in url: | |
track_id = extract_amazon_track_id(url) | |
if track_id: | |
# Use the working format for Amazon Music tracks | |
api_url = f"https://api.song.link/v1-alpha.1/links?type=song&platform=amazonMusic&id={track_id}&userCountry=US" | |
else: | |
# If no track ID is found, use the original URL | |
api_url = f"https://api.song.link/v1-alpha.1/links?url={url}&userCountry=US" | |
else: | |
# For non-Amazon Music URLs, use the standard format | |
api_url = f"https://api.song.link/v1-alpha.1/links?url={url}&userCountry=US" | |
# Make the API call | |
response = requests.get(api_url) | |
if response.status_code == 200: | |
track_info = response | |
if not track_info: | |
raise HTTPException(status_code=404, detail="Could not fetch track info") | |
entityUniqueId = track_info["entityUniqueId"] | |
title = track_info["entitiesByUniqueId"][entityUniqueId]["title"] | |
artist = track_info["entitiesByUniqueId"][entityUniqueId]["artistName"] | |
filename = f"{title} - {artist}" | |
#extract YouTube URL | |
youtube_url = extract_url(track_info["linksByPlatform"], "youtube") | |
if youtube_url: | |
if title and artist: | |
filename = f"{title} - {artist}" | |
return {"url": youtube_url, "filename": filename} | |
else: | |
return {"url": youtube_url, "filename": "Unknown Track - Unknown Artist"} | |
else: | |
return None | |
else: | |
return None | |
''' | |
else: | |
query = f'{title}+{artist}' | |
logger.info(f"search query: {query}") | |
search_results = ytmusic.search(query, filter="songs") | |
first_song = next((song for song in search_results if 'videoId' in song and song['videoId']), {}) if search_results else {} | |
if 'videoId' in first_song: | |
videoId = first_song["videoId"] | |
ym_url = f'https://www.youtube.com/watch?v={videoId}' | |
return {"filename": filename, "url": ym_url, "track_id": videoId} | |
''' | |
async def health_check(): | |
"""Health check endpoint.""" | |
try: | |
# Test Spotify API token generation | |
token = get_spotify_token() | |
return { | |
"status": "healthy", | |
"spotify_auth": "ok", | |
"token_expires_in": token_cache.expiry_time.timestamp() - datetime.now().timestamp() if token_cache.expiry_time else None | |
} | |
except Exception as e: | |
logger.error(f"Health check failed: {str(e)}") | |
return {"status": "unhealthy", "error": str(e)} | |