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:
2
.gitignore
vendored
2
.gitignore
vendored
@ -170,4 +170,4 @@ cython_debug/
|
|||||||
|
|
||||||
venv/*
|
venv/*
|
||||||
Documents.md
|
Documents.md
|
||||||
test.py
|
test.py
|
||||||
|
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"]
|
12
README.md
12
README.md
@ -204,18 +204,18 @@ try:
|
|||||||
# Step 1: Initiate login
|
# Step 1: Initiate login
|
||||||
verify_id = client.auth.login("960", "7777777")
|
verify_id = client.auth.login("960", "7777777")
|
||||||
print(f"OTP sent! Verification ID: {verify_id}")
|
print(f"OTP sent! Verification ID: {verify_id}")
|
||||||
|
|
||||||
# Step 2: Get OTP from user
|
# Step 2: Get OTP from user
|
||||||
otp = input("Enter OTP: ")
|
otp = input("Enter OTP: ")
|
||||||
|
|
||||||
# Step 3: Verify OTP and get token
|
# Step 3: Verify OTP and get token
|
||||||
token = client.auth.verify_otp(otp, verify_id)
|
token = client.auth.verify_otp(otp, verify_id)
|
||||||
print(f"Login successful! Token: {token}")
|
print(f"Login successful! Token: {token}")
|
||||||
|
|
||||||
# Step 4: Use the API
|
# Step 4: Use the API
|
||||||
user_info = client.user.get_me()
|
user_info = client.user.get_me()
|
||||||
print(f"Welcome, {user_info['username']}!")
|
print(f"Welcome, {user_info['username']}!")
|
||||||
|
|
||||||
except AuthenticationError as e:
|
except AuthenticationError as e:
|
||||||
print(f"Authentication failed: {e}")
|
print(f"Authentication failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
@ -232,7 +232,7 @@ for category in results:
|
|||||||
print(f"\n{category['title']}:")
|
print(f"\n{category['title']}:")
|
||||||
for item in category['items'][:3]: # Show top 3
|
for item in category['items'][:3]: # Show top 3
|
||||||
print(f" {item['heading']} - {item['sub_heading']}")
|
print(f" {item['heading']} - {item['sub_heading']}")
|
||||||
|
|
||||||
# Get detailed info if it's a song
|
# Get detailed info if it's a song
|
||||||
if category['title'] == 'Songs':
|
if category['title'] == 'Songs':
|
||||||
try:
|
try:
|
||||||
@ -281,4 +281,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: This is an unofficial client library. songbox is a trademark of its respective owners.
|
**Note**: This is an unofficial client library. songbox is a trademark of its respective owners.
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
|
boto3==1.38.32
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
boto3==1.38.32
|
|
9
setup.py
9
setup.py
@ -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",
|
||||||
@ -59,4 +56,4 @@ setup(
|
|||||||
"Source": "https://git.cubable.date/CustomIcon/songbox",
|
"Source": "https://git.cubable.date/CustomIcon/songbox",
|
||||||
"Documentation": "https://git.cubable.date/CustomIcon/songbox/src/branch/master/README.md",
|
"Documentation": "https://git.cubable.date/CustomIcon/songbox/src/branch/master/README.md",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
@ -29,4 +27,4 @@ __all__ = [
|
|||||||
"APIError",
|
"APIError",
|
||||||
"RateLimitError",
|
"RateLimitError",
|
||||||
"NotFoundError",
|
"NotFoundError",
|
||||||
]
|
]
|
||||||
|
@ -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:
|
||||||
@ -20,98 +20,110 @@ Examples:
|
|||||||
songbox auth login --country-code 960 --mobile 7777777
|
songbox auth login --country-code 960 --mobile 7777777
|
||||||
songbox auth verify --otp 123456 --verify-id <verify_id>
|
songbox auth verify --otp 123456 --verify-id <verify_id>
|
||||||
songbox user me
|
songbox user me
|
||||||
|
|
||||||
# Search for music
|
# Search for music
|
||||||
songbox music search "huttaa"
|
songbox music search "huttaa"
|
||||||
|
|
||||||
# Get album information
|
# Get album information
|
||||||
songbox music album 808
|
songbox music album 808
|
||||||
|
|
||||||
# Download a song
|
# Download a song
|
||||||
songbox music download "https://example.com/song.mp3"
|
songbox music download "https://example.com/song.mp3"
|
||||||
songbox music download "https://example.com/song.mp3" --path ./my_music
|
songbox music download "https://example.com/song.mp3" --path ./my_music
|
||||||
|
|
||||||
# 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")
|
||||||
|
|
||||||
# Auth commands
|
# Auth commands
|
||||||
auth_parser = subparsers.add_parser("auth", help="Authentication commands")
|
auth_parser = subparsers.add_parser("auth", help="Authentication commands")
|
||||||
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")
|
||||||
|
|
||||||
# Logout command
|
# Logout command
|
||||||
auth_subparsers.add_parser("logout", help="Logout (clear token)")
|
auth_subparsers.add_parser("logout", help="Logout (clear token)")
|
||||||
|
|
||||||
# User commands
|
# User commands
|
||||||
user_parser = subparsers.add_parser("user", help="User commands")
|
user_parser = subparsers.add_parser("user", help="User commands")
|
||||||
user_subparsers = user_parser.add_subparsers(dest="user_command")
|
user_subparsers = user_parser.add_subparsers(dest="user_command")
|
||||||
|
|
||||||
user_subparsers.add_parser("me", help="Get current user information")
|
user_subparsers.add_parser("me", help="Get current user information")
|
||||||
user_subparsers.add_parser("following", help="Get following list")
|
user_subparsers.add_parser("following", help="Get following list")
|
||||||
|
|
||||||
# Music commands
|
# Music commands
|
||||||
music_parser = subparsers.add_parser("music", help="Music commands")
|
music_parser = subparsers.add_parser("music", help="Music commands")
|
||||||
music_subparsers = music_parser.add_subparsers(dest="music_command")
|
music_subparsers = music_parser.add_subparsers(dest="music_command")
|
||||||
|
|
||||||
# Search command
|
# Search command
|
||||||
search_parser = music_subparsers.add_parser("search", help="Search for music")
|
search_parser = music_subparsers.add_parser("search", help="Search for music")
|
||||||
search_parser.add_argument("query", help="Search query")
|
search_parser.add_argument("query", help="Search query")
|
||||||
|
|
||||||
# Album command
|
# Album command
|
||||||
album_parser = music_subparsers.add_parser("album", help="Get album information")
|
album_parser = music_subparsers.add_parser("album", help="Get album information")
|
||||||
album_parser.add_argument("album_id", help="Album ID")
|
album_parser.add_argument("album_id", help="Album ID")
|
||||||
|
|
||||||
# Song command
|
# Song command
|
||||||
song_parser = music_subparsers.add_parser("song", help="Get song information")
|
song_parser = music_subparsers.add_parser("song", help="Get song information")
|
||||||
song_parser.add_argument("song_id", help="Song ID")
|
song_parser.add_argument("song_id", help="Song ID")
|
||||||
|
|
||||||
# Artist command
|
# Artist command
|
||||||
artist_parser = music_subparsers.add_parser("artist", help="Get artist information")
|
artist_parser = music_subparsers.add_parser("artist", help="Get artist information")
|
||||||
artist_parser.add_argument("artist_id", help="Artist ID")
|
artist_parser.add_argument("artist_id", help="Artist ID")
|
||||||
|
|
||||||
# Trending command
|
# Trending command
|
||||||
trending_parser = music_subparsers.add_parser("trending", help="Get trending music")
|
trending_parser = music_subparsers.add_parser("trending", help="Get trending music")
|
||||||
trending_parser.add_argument("--limit", type=int, help="Limit number of results")
|
trending_parser.add_argument("--limit", type=int, help="Limit number of results")
|
||||||
|
|
||||||
# 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,21 +171,25 @@ 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)
|
||||||
|
|
||||||
elif args.auth_command == "verify":
|
elif args.auth_command == "verify":
|
||||||
try:
|
try:
|
||||||
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)
|
||||||
|
|
||||||
elif args.auth_command == "refresh":
|
elif args.auth_command == "refresh":
|
||||||
try:
|
try:
|
||||||
new_token = client.auth.refresh_token()
|
new_token = client.auth.refresh_token()
|
||||||
@ -182,7 +198,7 @@ def handle_auth_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Token refresh failed: {e}", file=sys.stderr)
|
print(f"Token refresh failed: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.auth_command == "logout":
|
elif args.auth_command == "logout":
|
||||||
client.auth.logout()
|
client.auth.logout()
|
||||||
print("Logged out successfully!")
|
print("Logged out successfully!")
|
||||||
@ -197,7 +213,7 @@ def handle_user_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get user info: {e}", file=sys.stderr)
|
print(f"Failed to get user info: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.user_command == "following":
|
elif args.user_command == "following":
|
||||||
try:
|
try:
|
||||||
following = client.user.get_following()
|
following = client.user.get_following()
|
||||||
@ -222,7 +238,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Search failed: {e}", file=sys.stderr)
|
print(f"Search failed: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.music_command == "album":
|
elif args.music_command == "album":
|
||||||
try:
|
try:
|
||||||
album = client.music.get_album(args.album_id)
|
album = client.music.get_album(args.album_id)
|
||||||
@ -230,7 +246,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get album: {e}", file=sys.stderr)
|
print(f"Failed to get album: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.music_command == "song":
|
elif args.music_command == "song":
|
||||||
try:
|
try:
|
||||||
song = client.music.get_song(args.song_id)
|
song = client.music.get_song(args.song_id)
|
||||||
@ -238,7 +254,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get song: {e}", file=sys.stderr)
|
print(f"Failed to get song: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.music_command == "artist":
|
elif args.music_command == "artist":
|
||||||
try:
|
try:
|
||||||
artist = client.music.get_artist(args.artist_id)
|
artist = client.music.get_artist(args.artist_id)
|
||||||
@ -246,7 +262,7 @@ def handle_music_commands(client: SongboxClient, args) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get artist: {e}", file=sys.stderr)
|
print(f"Failed to get artist: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elif args.music_command == "download":
|
elif args.music_command == "download":
|
||||||
try:
|
try:
|
||||||
file_path = client.music.download_song(args.song_url, args.path)
|
file_path = client.music.download_song(args.song_url, args.path)
|
||||||
@ -257,42 +273,48 @@ 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()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not args.command:
|
if not args.command:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 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
|
||||||
try:
|
try:
|
||||||
client = SongboxClient(base_url=args.base_url)
|
client = SongboxClient(base_url=args.base_url)
|
||||||
|
|
||||||
# Set token if provided
|
# Set token if provided
|
||||||
if token:
|
if token:
|
||||||
client.set_token(token)
|
client.set_token(token)
|
||||||
|
|
||||||
# Handle commands
|
# Handle commands
|
||||||
if args.command == "auth":
|
if args.command == "auth":
|
||||||
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)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nOperation cancelled.", file=sys.stderr)
|
print("\nOperation cancelled.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -300,9 +322,9 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
"""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:
|
||||||
"""Main client for interacting with the songbox API.
|
"""Main client for interacting with the songbox API.
|
||||||
|
|
||||||
This client provides access to all songbox API endpoints through
|
This client provides access to all songbox API endpoints through
|
||||||
modular sub-clients for authentication, user management, and music.
|
modular sub-clients for authentication, user management, and music.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
base_url: The base URL for the songbox API
|
base_url: The base URL for the songbox API
|
||||||
timeout: Request timeout in seconds (default: 30)
|
timeout: Request timeout in seconds (default: 30)
|
||||||
headers: Additional headers to include with requests
|
headers: Additional headers to include with requests
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> client = SongboxClient()
|
>>> client = SongboxClient()
|
||||||
>>> # Authenticate
|
>>> # Authenticate
|
||||||
@ -29,62 +29,62 @@ class SongboxClient:
|
|||||||
>>> user_info = client.user.get_me()
|
>>> user_info = client.user.get_me()
|
||||||
>>> print(user_info)
|
>>> print(user_info)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
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
|
||||||
default_headers = {
|
default_headers = {
|
||||||
"User-Agent": "songbox/2.1.4 (songbox; Python) httpx",
|
"User-Agent": "songbox/2.1.4 (songbox; Python) httpx",
|
||||||
"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:
|
||||||
default_headers.update(headers)
|
default_headers.update(headers)
|
||||||
self._http_client = httpx.Client(
|
self._http_client = httpx.Client(
|
||||||
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)
|
||||||
self.music = MusicClient(self)
|
self.music = MusicClient(self)
|
||||||
|
|
||||||
def set_token(self, token: str) -> None:
|
def set_token(self, token: str) -> None:
|
||||||
"""Set the authentication token for API requests.
|
"""Set the authentication token for API requests.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token: JWT token obtained from authentication
|
token: JWT token obtained from authentication
|
||||||
"""
|
"""
|
||||||
self._token = token
|
self._token = token
|
||||||
self._http_client.headers["Authorization"] = f"Bearer {token}"
|
self._http_client.headers["Authorization"] = f"Bearer {token}"
|
||||||
|
|
||||||
def clear_token(self) -> None:
|
def clear_token(self) -> None:
|
||||||
"""Clear the authentication token."""
|
"""Clear the authentication token."""
|
||||||
self._token = None
|
self._token = None
|
||||||
if "Authorization" in self._http_client.headers:
|
if "Authorization" in self._http_client.headers:
|
||||||
del self._http_client.headers["Authorization"]
|
del self._http_client.headers["Authorization"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def token(self) -> Optional[str]:
|
def token(self) -> Optional[str]:
|
||||||
"""Get the current authentication token."""
|
"""Get the current authentication token."""
|
||||||
return self._token
|
return self._token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_authenticated(self) -> bool:
|
def is_authenticated(self) -> bool:
|
||||||
"""Check if the client is authenticated."""
|
"""Check if the client is authenticated."""
|
||||||
return self._token is not None
|
return self._token is not None
|
||||||
|
|
||||||
def request(
|
def request(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
@ -93,10 +93,10 @@ 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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
method: HTTP method (GET, POST, etc.)
|
method: HTTP method (GET, POST, etc.)
|
||||||
endpoint: API endpoint (without base URL)
|
endpoint: API endpoint (without base URL)
|
||||||
@ -105,19 +105,19 @@ class SongboxClient:
|
|||||||
json: JSON data for POST requests
|
json: JSON data for POST requests
|
||||||
headers: Additional headers
|
headers: Additional headers
|
||||||
require_auth: Whether authentication is required
|
require_auth: Whether authentication is required
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON response data
|
JSON response data
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
songboxError: If authentication is required but not provided
|
songboxError: If authentication is required but not provided
|
||||||
APIError: If the API returns an error response
|
APIError: If the API returns an error response
|
||||||
"""
|
"""
|
||||||
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(
|
||||||
method=method,
|
method=method,
|
||||||
@ -125,34 +125,34 @@ 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()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return response.json()
|
return response.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
raise songboxError(f"Request failed: {str(e)}")
|
raise songboxError(f"Request failed: {str(e)}")
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close the HTTP client."""
|
"""Close the HTTP client."""
|
||||||
self._http_client.close()
|
self._http_client.close()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
self.close()
|
self.close()
|
||||||
|
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"""
|
"""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:
|
@ -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:
|
@ -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:
|
@ -5,29 +5,30 @@ 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
|
||||||
|
|
||||||
|
|
||||||
class APIError(songboxError):
|
class APIError(songboxError):
|
||||||
"""Exception raised when the API returns an error response.
|
"""Exception raised when the API returns an error response.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: Error message
|
message: Error message
|
||||||
status_code: HTTP status code
|
status_code: HTTP status code
|
||||||
response_data: Raw response data from the API
|
response_data: Raw response data from the API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
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
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
self.response_data = response_data or {}
|
self.response_data = response_data or {}
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.status_code:
|
if self.status_code:
|
||||||
return f"API Error {self.status_code}: {self.message}"
|
return f"API Error {self.status_code}: {self.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
|
||||||
|
Reference in New Issue
Block a user