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:
35
.pre-commit-config.yaml
Normal file
35
.pre-commit-config.yaml
Normal 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"]
|
@ -1,2 +1,2 @@
|
||||
httpx==0.28.1
|
||||
boto3==1.38.32
|
||||
httpx==0.28.1
|
||||
|
7
setup.py
7
setup.py
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
6
songbox/clients/__init__.py
Normal file
6
songbox/clients/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from .music import MusicClient
|
||||
from .user import UserClient
|
||||
from .auth import AuthClient
|
||||
|
||||
|
||||
__all__ = ["AuthClient", "UserClient", "MusicClient"]
|
@ -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:
|
@ -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:
|
@ -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:
|
@ -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
|
Reference in New Issue
Block a user