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
httpx==0.28.1

View File

@ -1,6 +1,6 @@
"""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:
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:
# requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
requirements = [
"httpx==0.28.1",
"boto3==1.38.32"
]
requirements = ["httpx==0.28.1", "boto3==1.38.32"]
setup(
name="songbox",

View File

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

View File

@ -3,9 +3,9 @@
import argparse
import json
import sys
from typing import Optional
from .client import SongboxClient
from .exceptions import songboxError, AuthenticationError
from .exceptions import AuthenticationError, songboxError
def create_parser() -> argparse.ArgumentParser:
@ -33,25 +33,25 @@ Examples:
# Get user's following list
songbox user following
"""
""",
)
parser.add_argument(
"--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(
"--base-url",
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(
"--format",
choices=["json", "table", "simple"],
default="simple",
help="Output format (default: simple)"
help="Output format (default: simple)",
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
@ -61,14 +61,22 @@ Examples:
auth_subparsers = auth_parser.add_subparsers(dest="auth_command")
# Login command
login_parser = auth_subparsers.add_parser("login", help="Initiate login with mobile number")
login_parser.add_argument("--country-code", required=True, help="Country code (e.g., 960)")
login_parser = auth_subparsers.add_parser(
"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")
# 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("--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
auth_subparsers.add_parser("refresh", help="Refresh authentication token")
@ -109,8 +117,12 @@ Examples:
# Download command
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("--path", help="Download directory path (default: ./downloads)")
download_parser.add_argument(
"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
@ -159,7 +171,9 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
try:
verify_id = client.auth.login(args.country_code, args.mobile)
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:
print(f"Login failed: {e}", file=sys.stderr)
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)
print(f"Authentication successful!")
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:
print(f"Verification failed: {e}", file=sys.stderr)
sys.exit(1)
@ -257,7 +273,6 @@ def handle_music_commands(client: SongboxClient, args) -> None:
sys.exit(1)
def main():
"""Main CLI entry point."""
parser = create_parser()
@ -269,6 +284,7 @@ def main():
# Get token from args or environment
import os
token = args.token or os.getenv("songbox_TOKEN")
# Create client
@ -284,12 +300,18 @@ def main():
handle_auth_commands(client, args)
elif args.command == "user":
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)
handle_user_commands(client, args)
elif args.command == "music":
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)
handle_music_commands(client, args)
@ -300,7 +322,7 @@ def main():
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
finally:
if 'client' in locals():
if "client" in locals():
client.close()

View File

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

View File

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

View File

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

View File

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