This commit introduces the initial version of the songbox API client library, including: - Core client implementation with authentication and HTTP handling - Modular sub-clients for auth, user, and music operations - Comprehensive exception hierarchy for error handling - CLI interface with support for all API operations - Setup configuration with dependencies and package metadata - Documentation including README with usage examples
247 lines
8.5 KiB
Python
247 lines
8.5 KiB
Python
"""Music client for songbox API"""
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
from .exceptions import AuthenticationError, NotFoundError, ValidationError
|
|
|
|
|
|
class MusicClient:
|
|
"""Client for handling music-related operations with the songbox API.
|
|
|
|
This client handles search, albums, songs, and other music operations.
|
|
"""
|
|
|
|
def __init__(self, client):
|
|
self._client = client
|
|
|
|
def search(self, query: str) -> List[Dict[str, Any]]:
|
|
"""Search for songs, artists, and albums.
|
|
|
|
Args:
|
|
query: Search query string
|
|
|
|
Returns:
|
|
List of search results categorized by type (Songs, Artists, Albums)
|
|
|
|
Raises:
|
|
ValidationError: If query is empty
|
|
AuthenticationError: If not authenticated
|
|
|
|
Example:
|
|
>>> results = client.music.search("huttaa")
|
|
>>> for category in results:
|
|
... print(f"Category: {category['title']}")
|
|
... for item in category['items']:
|
|
... print(f" - {item['heading']}")
|
|
"""
|
|
if not query or not query.strip():
|
|
raise ValidationError("Search query cannot be empty")
|
|
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint=f"/v1/search/{query.strip()}",
|
|
require_auth=True
|
|
)
|
|
|
|
# Ensure we return a list
|
|
if isinstance(response, list):
|
|
return response
|
|
elif isinstance(response, dict):
|
|
# If response is a dict, try to extract results
|
|
return response.get('results', response.get('data', []))
|
|
else:
|
|
return []
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
raise
|
|
|
|
def get_album(self, album_id: str) -> Dict[str, Any]:
|
|
"""Get detailed information about an album.
|
|
|
|
Args:
|
|
album_id: ID of the album
|
|
|
|
Returns:
|
|
Album information including songs, artist, artwork, etc.
|
|
|
|
Raises:
|
|
ValidationError: If album_id is empty
|
|
AuthenticationError: If not authenticated
|
|
NotFoundError: If album not found
|
|
|
|
Example:
|
|
>>> album = client.music.get_album("808")
|
|
>>> print(f"Album: {album['name']} by {album['artist_name']}")
|
|
>>> print(f"Songs: {len(album['songs'])}")
|
|
"""
|
|
if not album_id or not str(album_id).strip():
|
|
raise ValidationError("Album ID cannot be empty")
|
|
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint=f"/v1/albums/{album_id}",
|
|
require_auth=True
|
|
)
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
elif "404" in str(e) or "not found" in str(e).lower():
|
|
raise NotFoundError(f"Album with ID {album_id} not found")
|
|
raise
|
|
|
|
def get_song(self, song_id: str) -> Dict[str, Any]:
|
|
"""Get detailed information about a song.
|
|
|
|
Args:
|
|
song_id: ID of the song
|
|
|
|
Returns:
|
|
Song information including artist, album, duration, URLs, etc.
|
|
|
|
Raises:
|
|
ValidationError: If song_id is empty
|
|
AuthenticationError: If not authenticated
|
|
NotFoundError: If song not found
|
|
|
|
Example:
|
|
>>> song = client.music.get_song("6200")
|
|
>>> print(f"Song: {song['name']} by {song['artist_name']}")
|
|
>>> print(f"Duration: {song['duration']} seconds")
|
|
"""
|
|
if not song_id or not str(song_id).strip():
|
|
raise ValidationError("Song ID cannot be empty")
|
|
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint=f"/v1/songs/{song_id}",
|
|
require_auth=True
|
|
)
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
elif "404" in str(e) or "not found" in str(e).lower():
|
|
raise NotFoundError(f"Song with ID {song_id} not found")
|
|
raise
|
|
|
|
def get_artist(self, artist_id: str) -> Dict[str, Any]:
|
|
"""Get detailed information about an artist.
|
|
|
|
Args:
|
|
artist_id: ID of the artist
|
|
|
|
Returns:
|
|
Artist information including albums, songs, etc.
|
|
|
|
Raises:
|
|
ValidationError: If artist_id is empty
|
|
AuthenticationError: If not authenticated
|
|
NotFoundError: If artist not found
|
|
|
|
Example:
|
|
>>> artist = client.music.get_artist("124")
|
|
>>> print(f"Artist: {artist['name']}")
|
|
"""
|
|
if not artist_id or not str(artist_id).strip():
|
|
raise ValidationError("Artist ID cannot be empty")
|
|
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint=f"/v1/artists/{artist_id}",
|
|
require_auth=True
|
|
)
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
elif "404" in str(e) or "not found" in str(e).lower():
|
|
raise NotFoundError(f"Artist with ID {artist_id} not found")
|
|
raise
|
|
|
|
def get_recommendations(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
"""Get personalized music recommendations.
|
|
|
|
Args:
|
|
limit: Maximum number of items to return
|
|
|
|
Returns:
|
|
List of recommended music items
|
|
|
|
Raises:
|
|
AuthenticationError: If not authenticated
|
|
|
|
Example:
|
|
>>> recommendations = client.music.get_recommendations(limit=5)
|
|
>>> for item in recommendations:
|
|
... print(f"Recommended: {item.get('name', 'Unknown')}")
|
|
"""
|
|
params = {}
|
|
if limit is not None:
|
|
params['limit'] = limit
|
|
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint="/v1/recommendations",
|
|
params=params,
|
|
require_auth=True
|
|
)
|
|
|
|
# Ensure we return a list
|
|
if isinstance(response, list):
|
|
return response
|
|
elif isinstance(response, dict):
|
|
return response.get('data', response.get('items', []))
|
|
else:
|
|
return []
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
raise
|
|
|
|
def get_playlists(self) -> List[Dict[str, Any]]:
|
|
"""Get user's playlists.
|
|
|
|
Returns:
|
|
List of user's playlists
|
|
|
|
Raises:
|
|
AuthenticationError: If not authenticated
|
|
|
|
Example:
|
|
>>> playlists = client.music.get_playlists()
|
|
>>> for playlist in playlists:
|
|
... print(f"Playlist: {playlist.get('name', 'Unknown')}")
|
|
"""
|
|
try:
|
|
response = self._client.request(
|
|
method="GET",
|
|
endpoint="/v1/playlists",
|
|
require_auth=True
|
|
)
|
|
|
|
# Ensure we return a list
|
|
if isinstance(response, list):
|
|
return response
|
|
elif isinstance(response, dict):
|
|
return response.get('data', response.get('playlists', []))
|
|
else:
|
|
return []
|
|
|
|
except Exception as e:
|
|
if "401" in str(e) or "unauthorized" in str(e).lower():
|
|
raise AuthenticationError("Authentication required or token expired")
|
|
raise |