refactor(clients): reorganize client modules into clients package

- Move auth, user, and music clients into new clients package
- Update imports to use new module paths
- Add __init__.py to expose clients from package
- Clean up code formatting and imports
- Add pre-commit configuration for code quality checks
This commit is contained in:
2025-06-07 06:57:01 -07:00
parent eac722e37f
commit 161a4d42d6
14 changed files with 191 additions and 128 deletions

35
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,35 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
- id: file-contents-sorter
- id: mixed-line-ending
- id: name-tests-test
- id: pretty-format-json
args: [--autofix]
- id: debug-statements
- id: check-docstring-first
- id: requirements-txt-fixer
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/isort
rev: 6.0.1
hooks:
- id: isort
args: ["--profile", "black"]

View File

@ -1,2 +1,2 @@
httpx==0.28.1
boto3==1.38.32 boto3==1.38.32
httpx==0.28.1

View File

@ -1,6 +1,6 @@
"""Setup script for songbox API Client Library""" """Setup script for songbox API Client Library"""
from setuptools import setup, find_packages from setuptools import find_packages, setup
with open("README.md", "r", encoding="utf-8") as fh: with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
@ -8,10 +8,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
# with open("songbox/requirements.txt", "r", encoding="utf-8") as fh: # with open("songbox/requirements.txt", "r", encoding="utf-8") as fh:
# requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")] # requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
requirements = [ requirements = ["httpx==0.28.1", "boto3==1.38.32"]
"httpx==0.28.1",
"boto3==1.38.32"
]
setup( setup(
name="songbox", name="songbox",

View File

@ -4,15 +4,13 @@ A Python client library for the songbox music streaming API.
""" """
from .client import SongboxClient from .client import SongboxClient
from .auth import AuthClient from .clients import AuthClient, MusicClient, UserClient
from .user import UserClient
from .music import MusicClient
from .exceptions import ( from .exceptions import (
songboxError,
AuthenticationError,
APIError, APIError,
AuthenticationError,
NotFoundError,
RateLimitError, RateLimitError,
NotFoundError songboxError,
) )
__version__ = "1.0.0" __version__ = "1.0.0"

View File

@ -3,9 +3,9 @@
import argparse import argparse
import json import json
import sys import sys
from typing import Optional
from .client import SongboxClient from .client import SongboxClient
from .exceptions import songboxError, AuthenticationError from .exceptions import AuthenticationError, songboxError
def create_parser() -> argparse.ArgumentParser: def create_parser() -> argparse.ArgumentParser:
@ -33,25 +33,25 @@ Examples:
# Get user's following list # Get user's following list
songbox user following songbox user following
""" """,
) )
parser.add_argument( parser.add_argument(
"--token", "--token",
help="Authentication token (can also be set via songbox_TOKEN env var)" help="Authentication token (can also be set via songbox_TOKEN env var)",
) )
parser.add_argument( parser.add_argument(
"--base-url", "--base-url",
default="https://lavafoshi.online", default="https://lavafoshi.online",
help="Base URL for the API (default: https://lavafoshi.online)" help="Base URL for the API (default: https://lavafoshi.online)",
) )
parser.add_argument( parser.add_argument(
"--format", "--format",
choices=["json", "table", "simple"], choices=["json", "table", "simple"],
default="simple", default="simple",
help="Output format (default: simple)" help="Output format (default: simple)",
) )
subparsers = parser.add_subparsers(dest="command", help="Available commands") subparsers = parser.add_subparsers(dest="command", help="Available commands")
@ -61,14 +61,22 @@ Examples:
auth_subparsers = auth_parser.add_subparsers(dest="auth_command") auth_subparsers = auth_parser.add_subparsers(dest="auth_command")
# Login command # Login command
login_parser = auth_subparsers.add_parser("login", help="Initiate login with mobile number") login_parser = auth_subparsers.add_parser(
login_parser.add_argument("--country-code", required=True, help="Country code (e.g., 960)") "login", help="Initiate login with mobile number"
)
login_parser.add_argument(
"--country-code", required=True, help="Country code (e.g., 960)"
)
login_parser.add_argument("--mobile", required=True, help="Mobile number") login_parser.add_argument("--mobile", required=True, help="Mobile number")
# Verify OTP command # Verify OTP command
verify_parser = auth_subparsers.add_parser("verify", help="Verify OTP and get token") verify_parser = auth_subparsers.add_parser(
"verify", help="Verify OTP and get token"
)
verify_parser.add_argument("--otp", required=True, help="OTP code") verify_parser.add_argument("--otp", required=True, help="OTP code")
verify_parser.add_argument("--verify-id", required=True, help="Verification ID from login") verify_parser.add_argument(
"--verify-id", required=True, help="Verification ID from login"
)
# Refresh token command # Refresh token command
auth_subparsers.add_parser("refresh", help="Refresh authentication token") auth_subparsers.add_parser("refresh", help="Refresh authentication token")
@ -109,8 +117,12 @@ Examples:
# Download command # Download command
download_parser = music_subparsers.add_parser("download", help="Download a song") download_parser = music_subparsers.add_parser("download", help="Download a song")
download_parser.add_argument("song_url", help="Song URL to download (e.g., from song['url_original'])") download_parser.add_argument(
download_parser.add_argument("--path", help="Download directory path (default: ./downloads)") "song_url", help="Song URL to download (e.g., from song['url_original'])"
)
download_parser.add_argument(
"--path", help="Download directory path (default: ./downloads)"
)
return parser return parser
@ -159,7 +171,9 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
try: try:
verify_id = client.auth.login(args.country_code, args.mobile) verify_id = client.auth.login(args.country_code, args.mobile)
print(f"OTP sent! Verification ID: {verify_id}") print(f"OTP sent! Verification ID: {verify_id}")
print(f"Use this command to verify: songbox auth verify --otp <OTP> --verify-id {verify_id}") print(
f"Use this command to verify: songbox auth verify --otp <OTP> --verify-id {verify_id}"
)
except Exception as e: except Exception as e:
print(f"Login failed: {e}", file=sys.stderr) print(f"Login failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -169,7 +183,9 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
token = client.auth.verify_otp(args.otp, args.verify_id) token = client.auth.verify_otp(args.otp, args.verify_id)
print(f"Authentication successful!") print(f"Authentication successful!")
print(f"Token: {token}") print(f"Token: {token}")
print(f"Save this token and use it with --token flag or songbox_TOKEN env var") print(
f"Save this token and use it with --token flag or songbox_TOKEN env var"
)
except Exception as e: except Exception as e:
print(f"Verification failed: {e}", file=sys.stderr) print(f"Verification failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -257,7 +273,6 @@ def handle_music_commands(client: SongboxClient, args) -> None:
sys.exit(1) sys.exit(1)
def main(): def main():
"""Main CLI entry point.""" """Main CLI entry point."""
parser = create_parser() parser = create_parser()
@ -269,6 +284,7 @@ def main():
# Get token from args or environment # Get token from args or environment
import os import os
token = args.token or os.getenv("songbox_TOKEN") token = args.token or os.getenv("songbox_TOKEN")
# Create client # Create client
@ -284,12 +300,18 @@ def main():
handle_auth_commands(client, args) handle_auth_commands(client, args)
elif args.command == "user": elif args.command == "user":
if not client.is_authenticated: if not client.is_authenticated:
print("Authentication required. Please login first or provide a token.", file=sys.stderr) print(
"Authentication required. Please login first or provide a token.",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
handle_user_commands(client, args) handle_user_commands(client, args)
elif args.command == "music": elif args.command == "music":
if not client.is_authenticated: if not client.is_authenticated:
print("Authentication required. Please login first or provide a token.", file=sys.stderr) print(
"Authentication required. Please login first or provide a token.",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
handle_music_commands(client, args) handle_music_commands(client, args)
@ -300,7 +322,7 @@ def main():
print(f"Error: {e}", file=sys.stderr) print(f"Error: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
finally: finally:
if 'client' in locals(): if "client" in locals():
client.close() client.close()

View File

@ -1,11 +1,11 @@
"""Main songbox API Client""" """Main songbox API Client"""
from typing import Any, Dict, Optional
import httpx import httpx
from typing import Optional, Dict, Any
from .auth import AuthClient from .clients import AuthClient, MusicClient, UserClient
from .user import UserClient from .exceptions import APIError, songboxError
from .music import MusicClient
from .exceptions import songboxError, APIError
class SongboxClient: class SongboxClient:
@ -34,9 +34,9 @@ class SongboxClient:
self, self,
base_url: str = "https://lavafoshi.online", base_url: str = "https://lavafoshi.online",
timeout: float = 30.0, timeout: float = 30.0,
headers: Optional[Dict[str, str]] = None headers: Optional[Dict[str, str]] = None,
): ):
self.base_url = base_url.rstrip('/') self.base_url = base_url.rstrip("/")
self._token: Optional[str] = None self._token: Optional[str] = None
# Default headers # Default headers
@ -45,7 +45,7 @@ class SongboxClient:
"Accept": "*/*", "Accept": "*/*",
"Accept-Language": "en-US;q=1.0", "Accept-Language": "en-US;q=1.0",
"Accept-Encoding": "gzip;q=1.0, compress;q=0.5", "Accept-Encoding": "gzip;q=1.0, compress;q=0.5",
"Connection": "keep-alive" "Connection": "keep-alive",
} }
if headers: if headers:
@ -54,7 +54,7 @@ class SongboxClient:
base_url=self.base_url, base_url=self.base_url,
timeout=timeout, timeout=timeout,
headers=default_headers, headers=default_headers,
follow_redirects=True follow_redirects=True,
) )
self.auth = AuthClient(self) self.auth = AuthClient(self)
self.user = UserClient(self) self.user = UserClient(self)
@ -93,7 +93,7 @@ class SongboxClient:
data: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None,
json: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
require_auth: bool = True require_auth: bool = True,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make an HTTP request to the songbox API. """Make an HTTP request to the songbox API.
@ -116,7 +116,7 @@ class SongboxClient:
if require_auth and not self.is_authenticated: if require_auth and not self.is_authenticated:
raise songboxError("Authentication required for this endpoint") raise songboxError("Authentication required for this endpoint")
url = endpoint if endpoint.startswith('http') else f"/api{endpoint}" url = endpoint if endpoint.startswith("http") else f"/api{endpoint}"
try: try:
response = self._http_client.request( response = self._http_client.request(
@ -125,7 +125,7 @@ class SongboxClient:
params=params, params=params,
data=data, data=data,
json=json, json=json,
headers=headers headers=headers,
) )
response.raise_for_status() response.raise_for_status()
@ -138,9 +138,9 @@ class SongboxClient:
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
try: try:
error_data = e.response.json() error_data = e.response.json()
message = error_data.get('message', f'HTTP {e.response.status_code}') message = error_data.get("message", f"HTTP {e.response.status_code}")
except ValueError: except ValueError:
message = f'HTTP {e.response.status_code}' message = f"HTTP {e.response.status_code}"
raise APIError(message, status_code=e.response.status_code) raise APIError(message, status_code=e.response.status_code)

View File

@ -0,0 +1,6 @@
from .music import MusicClient
from .user import UserClient
from .auth import AuthClient
__all__ = ["AuthClient", "UserClient", "MusicClient"]

View File

@ -1,7 +1,7 @@
"""Authentication client for songbox API""" """Authentication client for songbox API"""
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from .exceptions import AuthenticationError, ValidationError from ..exceptions import AuthenticationError, ValidationError
class AuthClient: class AuthClient:

View File

@ -5,7 +5,7 @@ import httpx
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .exceptions import AuthenticationError, NotFoundError, ValidationError from ..exceptions import AuthenticationError, NotFoundError, ValidationError
class MusicClient: class MusicClient:

View File

@ -1,7 +1,7 @@
"""User client for songbox API""" """User client for songbox API"""
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .exceptions import AuthenticationError, NotFoundError from ..exceptions import AuthenticationError, NotFoundError
class UserClient: class UserClient:

View File

@ -5,6 +5,7 @@ from typing import Optional
class songboxError(Exception): class songboxError(Exception):
"""Base exception for all songbox API errors.""" """Base exception for all songbox API errors."""
pass pass
@ -21,7 +22,7 @@ class APIError(songboxError):
self, self,
message: str, message: str,
status_code: Optional[int] = None, status_code: Optional[int] = None,
response_data: Optional[dict] = None response_data: Optional[dict] = None,
): ):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
@ -36,19 +37,23 @@ class APIError(songboxError):
class AuthenticationError(songboxError): class AuthenticationError(songboxError):
"""Exception raised for authentication-related errors.""" """Exception raised for authentication-related errors."""
pass pass
class RateLimitError(APIError): class RateLimitError(APIError):
"""Exception raised when rate limit is exceeded.""" """Exception raised when rate limit is exceeded."""
pass pass
class NotFoundError(APIError): class NotFoundError(APIError):
"""Exception raised when a resource is not found (404).""" """Exception raised when a resource is not found (404)."""
pass pass
class ValidationError(songboxError): class ValidationError(songboxError):
"""Exception raised for input validation errors.""" """Exception raised for input validation errors."""
pass pass