Files
deezspot-spotizerr-dev/deezspot/easy_spoty.py

343 lines
12 KiB
Python

#!/usr/bin/python3
from spotipy import Spotify
from deezspot.exceptions import InvalidLink
from spotipy.exceptions import SpotifyException
from spotipy.oauth2 import SpotifyClientCredentials
import os
class Spo:
__error_codes = [404, 400]
# Class-level API instance and credentials
__api = None
__client_id = None
__client_secret = None
__initialized = False
@classmethod
def __init__(cls, client_id, client_secret):
"""
Initialize the Spotify API client.
Args:
client_id (str): Spotify API client ID.
client_secret (str): Spotify API client secret.
"""
if not client_id or not client_secret:
raise ValueError("Spotify API credentials required. Provide client_id and client_secret.")
client_credentials_manager = SpotifyClientCredentials(
client_id=client_id,
client_secret=client_secret
)
# Store the credentials and API instance
cls.__client_id = client_id
cls.__client_secret = client_secret
cls.__api = Spotify(
auth_manager=client_credentials_manager
)
cls.__initialized = True
@classmethod
def __check_initialized(cls):
"""Check if the class has been initialized with credentials"""
if not cls.__initialized:
raise ValueError("Spotify API not initialized. Call Spo.__init__(client_id, client_secret) first.")
@classmethod
def __get_api(cls, client_id=None, client_secret=None):
"""
Get a Spotify API instance with the provided credentials or use stored credentials.
Args:
client_id (str, optional): Spotify API client ID
client_secret (str, optional): Spotify API client secret
Returns:
A Spotify API instance
"""
# If new credentials are provided, create a new API instance
if client_id and client_secret:
client_credentials_manager = SpotifyClientCredentials(
client_id=client_id,
client_secret=client_secret
)
return Spotify(auth_manager=client_credentials_manager)
# Otherwise, use the existing class-level API
cls.__check_initialized()
return cls.__api
@classmethod
def __lazy(cls, results, api=None):
"""Process paginated results and extend the initial page's items in-place."""
api = api or cls.__api
if not results or 'items' not in results:
return results
items_ref = results['items']
while results.get('next'):
results = api.next(results)
if results and 'items' in results:
items_ref.extend(results['items'])
else:
break
return results
@classmethod
def __fetch_all_album_tracks(cls, album_id: str, api: Spotify) -> dict:
"""
Fetch all tracks for an album using album_tracks pagination.
Returns a dict shaped like Spotify's 'tracks' object with all items merged.
"""
all_items = []
limit = 50
offset = 0
first_page = None
while True:
page = api.album_tracks(album_id, limit=limit, offset=offset)
if first_page is None:
first_page = dict(page) if page is not None else None
items = page.get('items', []) if page else []
if not items:
break
all_items.extend(items)
offset += len(items)
if page.get('next') is None:
break
if first_page is None:
return {'items': [], 'total': 0, 'limit': limit, 'offset': 0}
# Build a consolidated tracks object
total_val = first_page.get('total', len(all_items))
return {
'items': all_items,
'total': total_val,
'limit': limit,
'offset': 0,
'next': None,
'previous': None
}
@classmethod
def get_track(cls, ids, client_id=None, client_secret=None):
"""
Get track information by ID.
Args:
ids (str): Spotify track ID
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Track information
"""
api = cls.__get_api(client_id, client_secret)
try:
track_json = api.track(ids)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
return track_json
@classmethod
def get_tracks(cls, ids: list, market: str = None, client_id=None, client_secret=None):
"""
Get information for multiple tracks by a list of IDs.
Handles chunking by 50 IDs per request and merges results while preserving order.
Args:
ids (list): A list of Spotify track IDs.
market (str, optional): An ISO 3166-1 alpha-2 country code.
client_id (str, optional): Optional custom Spotify client ID.
client_secret (str, optional): Optional custom Spotify client secret.
Returns:
dict: A dictionary containing a list of track information under key 'tracks'.
"""
if not ids:
return {'tracks': []}
api = cls.__get_api(client_id, client_secret)
all_tracks = []
chunk_size = 50
try:
for i in range(0, len(ids), chunk_size):
chunk = ids[i:i + chunk_size]
resp = api.tracks(chunk, market=market) if market else api.tracks(chunk)
# Spotify returns {'tracks': [...]} for each chunk
chunk_tracks = resp.get('tracks', []) if resp else []
all_tracks.extend(chunk_tracks)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
# Create a string of the first few IDs for the error message
ids_preview = ', '.join(ids[:3]) + ('...' if len(ids) > 3 else '')
raise InvalidLink(f"one or more IDs in the list: [{ids_preview}]")
else:
raise
return {'tracks': all_tracks}
@classmethod
def get_album(cls, ids, client_id=None, client_secret=None):
"""
Get album information by ID and include all tracks (paged if needed).
Args:
ids (str): Spotify album ID
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Album information with full 'tracks.items'
"""
api = cls.__get_api(client_id, client_secret)
try:
album_json = api.album(ids)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
else:
raise
# Replace/ensure tracks contains all items via dedicated pagination endpoint
try:
full_tracks_obj = cls.__fetch_all_album_tracks(ids, api)
if isinstance(album_json, dict):
album_json['tracks'] = full_tracks_obj
except Exception:
# Fallback to lazy-paging over embedded 'tracks' if available
try:
tracks = album_json.get('tracks') if isinstance(album_json, dict) else None
if tracks:
cls.__lazy(tracks, api)
except Exception:
pass
return album_json
@classmethod
def get_playlist(cls, ids, client_id=None, client_secret=None):
"""
Get playlist information by ID.
Args:
ids (str): Spotify playlist ID
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Playlist information
"""
api = cls.__get_api(client_id, client_secret)
try:
playlist_json = api.playlist(ids)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
tracks = playlist_json['tracks']
cls.__lazy(tracks, api)
return playlist_json
@classmethod
def get_episode(cls, ids, client_id=None, client_secret=None):
"""
Get episode information by ID.
Args:
ids (str): Spotify episode ID
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Episode information
"""
api = cls.__get_api(client_id, client_secret)
try:
episode_json = api.episode(ids)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
return episode_json
@classmethod
def search(cls, query, search_type='track', limit=10, client_id=None, client_secret=None):
"""
Search for tracks, albums, artists, or playlists.
Args:
query (str): Search query
search_type (str, optional): Type of search ('track', 'album', 'artist', 'playlist')
limit (int, optional): Maximum number of results to return
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Search results
"""
api = cls.__get_api(client_id, client_secret)
search = api.search(q=query, type=search_type, limit=limit)
return search
@classmethod
def get_artist(cls, ids, client_id=None, client_secret=None):
"""
Get artist information by ID.
Args:
ids (str): Spotify artist ID
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Artist information
"""
api = cls.__get_api(client_id, client_secret)
try:
artist_json = api.artist(ids)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
return artist_json
@classmethod
def get_artist_discography(cls, ids, album_type='album,single,compilation,appears_on', limit=50, offset=0, client_id=None, client_secret=None):
"""
Get artist information and discography by ID.
Args:
ids (str): Spotify artist ID
album_type (str, optional): Types of albums to include
limit (int, optional): Maximum number of results
client_id (str, optional): Optional custom Spotify client ID
client_secret (str, optional): Optional custom Spotify client secret
Returns:
dict: Artist discography
"""
api = cls.__get_api(client_id, client_secret)
try:
# Request all types of releases by the artist.
discography = api.artist_albums(
ids,
album_type=album_type,
limit=limit,
offset=offset
)
except SpotifyException as error:
if error.http_status in cls.__error_codes:
raise InvalidLink(ids)
else:
raise
# Ensure that all pages of results are fetched.
cls.__lazy(discography, api)
return discography