# slskr Client Libraries Client libraries for the slskr HTTP API, providing language-specific implementations with batch operations, WebSocket support, and comprehensive examples. ## Overview - **TypeScript/JavaScript**: Production-ready, full feature support - **Python**: Async/await support with aiohttp, batch operations, WebSocket events - **Go**: Concurrent operations, type-safe interfaces, batch builder pattern - **Rust**: Native integration with HTTP API (embedded) - **slskd automation clients**: Compatibility routes for existing `slskd_api` Python automations ## Python Client ### Installation ```bash # From source cd client-python pip install -e . # Or install dependencies pip install aiohttp ``` The maintained Python client supports Python 3.10 through 3.13 and publishes an sdist and wheel. Runtime dependencies are constrained to the supported `aiohttp` 3.x API family; use `client-python/constraints.txt` when resolving the development environment. ### Quick Start ```python import asyncio from slskr import SlskrClient async def main(): client = SlskrClient( base_url="http://127.0.0.1:5030", token="your-api-key-here" ) # Check server health health = await client.health() print(f"Server status: {health['status']}") # Create search search = await client.create_search("beethoven symphony") print(f"Search ID: {search['id']}") # Get search results details = await client.get_search_details(search['id']) print(f"Found {len(details['results'])} results") await client.close() asyncio.run(main()) ``` ### Features #### Core API Methods ```python # Health & Version await client.health() await client.version() # Configuration await client.get_config() await client.get_stats() # Search await client.list_searches(limit=50, offset=0) search = await client.create_search(query="artist name") await client.get_search_details(search_id, limit=50, offset=0) # Messages await client.list_messages(limit=50) await client.get_user_messages(username, limit=50, offset=0) await client.send_message(recipient, content) await client.acknowledge_message(message_id) # Transfers await client.list_transfers(direction="download", status="active") await client.create_transfer(direction, peer_username, filename) await client.get_transfer(transfer_id) await client.cancel_transfer(transfer_id) # Sessions, users, rooms, browse, shares, and filters await client.get_sessions() await client.list_users() await client.list_rooms() await client.browse_user(username) await client.get_events(event_type="search.started", topic="searches", query="ambient") await client.list_shares() await client.update_filters({"enabled": True}) ``` #### Batch Operations ```python # Build and execute batch operations batch = client.batch.builder() batch.get("/api/stats", op_id="stats") batch.post("/api/searches", {"query": "query"}, op_id="search") batch.put("/api/v0/config/download-filter", {"enabled": True}, op_id="filters") batch.delete("/api/transfers/123", op_id="cancel") response = await batch.execute() # Check results print(f"Successful: {len(response.get_successful())}") print(f"Failed: {len(response.get_failed())}") print(f"Total time: {response.total_time_ms}ms") ``` #### WebSocket Events ```python # Connect and subscribe to events ws = await client.connect_ws() # Define event handlers async def handle_message(event): print(f"Message: {event}") async def handle_search_result(event): print(f"Search result: {event.get('data', {})}") # Register listeners ws.on("message.received", handle_message) ws.on("search.result", handle_search_result) # Subscribe to topics ws.subscribe("message.received", "search.result", "transfer.started") # Listen for events (async) while ws.is_connected(): await asyncio.sleep(1) # Cleanup await client.disconnect_ws() ``` #### Context Manager ```python # Auto-cleanup with context manager async with SlskrClient("http://127.0.0.1:5030", "token") as client: health = await client.health() searches = await client.list_searches() # Auto-closed when exiting ``` #### Error Handling ```python from slskr import ApiError, NetworkError, TimeoutError try: search = await client.create_search("query") except ApiError as e: if e.is_not_found(): print("Endpoint not found") elif e.is_unauthorized(): print("Invalid API key") elif e.is_server_error(): print(f"Server error: {e.status}") except TimeoutError: print("Request timeout") except NetworkError as e: print(f"Network error: {e}") ``` ### Examples - `examples/basic_usage.py` - Essential API operations - `examples/websocket_events.py` - Real-time event handling - `examples/advanced_usage.py` - Error handling, retries, concurrency, pagination - `examples/integration_example.py` - Coordinated multi-feature operations ### API Reference **SlskrClient** ```python SlskrClient( base_url: str, # API server URL token: str, # API authentication token timeout: int = 30, # Request timeout in seconds retries: int = 3, # Automatic retry count retry_delay: int = 1, # Delay between retries debug: bool = False # Enable debug logging ) ``` **Methods**: All are async/await - `health()` → dict - `version()` → dict - `get_config()` → dict - `get_stats()` → dict - `list_searches(limit, offset)` → list - `create_search(query, room, target)` → dict - `get_search_details(search_id, limit, offset)` → dict - `list_messages(limit, offset)` → list - `get_user_messages(username, limit, offset)` → list - `send_message(recipient, content)` → dict - `acknowledge_message(message_id)` → None - `list_transfers(direction, status, limit, offset)` → list - `create_transfer(direction, peer_username, filename)` → dict - `get_transfer(transfer_id)` → dict - `cancel_transfer(transfer_id)` → None - `connect_ws()` → WebSocketClient - `disconnect_ws()` → None - `close()` → None ## slskd Automation Compatibility Existing automations that use the Python `slskd_api` package can point at `slskr` with the daemon base URL and API token. The compatibility surface covers the slskd-style application, session, server, search, transfer, room, conversation, user, share, file, relay, options, event, log, and telemetry calls exercised by the local smoke suite. ```python from slskd_api import SlskdClient client = SlskdClient( host="http://127.0.0.1:5030", api_key="your-api-token", ) print(client.application.state()) print(client.searches.get_all()) ``` Run the local compatibility smoke against a temporary authenticated daemon: ```bash SLSKR_SLSKD_API_SMOKE_TOKEN=slskd-api-smoke-token scripts/run-slskd-api-compat-smoke.sh ``` Useful overrides: - `SLSKR_SLSKD_API_SMOKE_TOKEN`: required bearer token for the temporary authenticated daemon. - `SLSKD_API_VERSION`: Python `slskd-api` package version, default `0.2.4`. - `SLSKD_API_PYTHONPATH`: preinstalled package directory to reuse instead of installing. - `SLSKR_SLSKD_API_SMOKE_PORT`: fixed local daemon port. - `SLSKR_SLSKD_API_SMOKE_DIR`: fixed temporary state/log directory. ## Go Client ### Installation ```bash # Add to go.mod require github.com/snapetech/slskr/client-go v0.1.0 # Or copy client files cp -r client-go/* your-project/ ``` ### Quick Start ```go package main import ( "context" "fmt" "log" "time" "github.com/snapetech/slskr/client-go" ) func main() { client := slskr.NewClient( "http://127.0.0.1:5030", "your-api-key-here", ) ctx := context.Background() // Check server health health, err := client.Health(ctx) if err != nil { log.Fatal(err) } fmt.Printf("Server status: %v\n", health["status"]) // Create search search, err := client.CreateSearch(ctx, "beethoven symphony") if err != nil { log.Fatal(err) } fmt.Printf("Search ID: %v\n", search["id"]) // List transfers transfers, err := client.ListTransfers(ctx, "download", "active", 10, 0) if err != nil { log.Fatal(err) } fmt.Printf("Active transfers: %d\n", len(transfers)) } ``` ### Features #### Core API Methods ```go // Health & Version client.Health(ctx) client.Version(ctx) // Configuration client.GetConfig(ctx) client.GetStats(ctx) client.GetCapabilities(ctx) // Search client.ListSearches(ctx, limit, offset) client.CreateSearch(ctx, query) client.CreateSearchWithOptions(ctx, query, slskr.SearchOptions{Room: room, Target: target}) client.GetSearchDetails(ctx, searchID, limit, offset) // Messages client.ListMessages(ctx, limit, offset) client.GetUserMessages(ctx, username, limit, offset) client.SendMessage(ctx, recipient, content) client.AcknowledgeMessage(ctx, messageID) // Transfers client.ListTransfers(ctx, direction, status, limit, offset) client.CreateTransfer(ctx, direction, peerUsername, filename) client.GetTransfer(ctx, transferID) client.CancelTransfer(ctx, transferID) // Users & Rooms client.GetUser(ctx, username) client.ListUsers(ctx, limit, offset) client.ListRooms(ctx[, limit, offset]) client.GetRoom(ctx, roomID) client.JoinRoom(ctx, roomName) client.LeaveRoom(ctx, roomID) client.BrowseUser(ctx, username, folder, limit, offset) client.RequestBrowse(ctx, username[, folder]) // Shares client.ListShares(ctx, limit, offset) client.RefreshShares(ctx) // Filters client.GetFilters(ctx) client.UpdateFilters(ctx, filters) ``` #### Batch Operations ```go batch := client.NewBatchBuilder() stats := "stats" config := "config" caps := "caps" batch.Get("/api/stats", &stats) batch.Get("/api/config", &config) batch.Get("/api/capabilities", &caps) batch.Post("/api/searches", map[string]interface{}{ "query": "search term", }, nil) batch.Delete("/api/transfers/123", nil) response, err := batch.Execute(ctx) if err != nil { log.Fatal(err) } fmt.Printf("Successful: %d\n", len(response.GetSuccessful())) fmt.Printf("Failed: %d\n", len(response.GetFailed())) fmt.Printf("Total time: %dms\n", response.TotalTimeMs) ``` #### WebSocket Events ```go ws := client.NewWebSocketClient(true) err := ws.Connect(ctx) if err != nil { log.Fatal(err) } defer ws.Disconnect(ctx) // Create event channels messageCh := make(chan interface{}, 100) connectionCh := make(chan bool, 10) errorCh := make(chan error, 10) // Register listeners ws.On("message.received", messageCh) ws.OnConnectionChange(connectionCh) ws.OnError(errorCh) // Subscribe to topics ws.Subscribe("message.received", "search.result", "transfer.started") // Listen for events for { select { case event := <-messageCh: fmt.Printf("Message: %v\n", event) case connected := <-connectionCh: fmt.Printf("Connected: %v\n", connected) case err := <-errorCh: fmt.Printf("Error: %v\n", err) case <-time.After(10 * time.Second): return } } ``` #### Concurrency ```go var wg sync.WaitGroup queries := []string{"bach", "mozart", "beethoven"} results := make(chan map[string]interface{}, len(queries)) for _, q := range queries { wg.Add(1) go func(query string) { defer wg.Done() search, err := client.CreateSearch(ctx, query) if err != nil { return } results <- search }(q) } go func() { wg.Wait() close(results) }() for result := range results { fmt.Printf("Search created: %v\n", result["id"]) } ``` ### Examples - `examples/basic_usage.go` - Essential API operations - `examples/websocket_events.go` - Real-time event handling - `examples/advanced_usage.go` - Concurrency, pagination, error handling, integration ### API Reference **NewClient** ```go NewClient(baseURL, token string) *Client ``` **Client Fields** ```go type Client struct { BaseURL string Token string HTTPClient *http.Client Timeout time.Duration } ``` **Batch Methods** ```go builder := client.NewBatchBuilder() builder.Get(path, opID) builder.Post(path, body, opID) builder.Put(path, body, opID) builder.Delete(path, opID) builder.Size() int builder.Clear() response, err := builder.Execute(ctx) ``` ## TypeScript/JavaScript Client See [client-ts/README.md](../client-ts/README.md) for complete documentation of the TypeScript client. ### Quick Installation ```bash npm install @slskr/api-client # or yarn add @slskr/api-client ``` ### Quick Usage ```typescript import SlskrClient from '@slskr/api-client'; const client = new SlskrClient({ baseUrl: 'http://127.0.0.1:5030', token: 'your-api-key' }); // Create search const search = await client.createSearch({ query: 'beethoven symphony' }); // Get results const results = await client.getSearchDetails(search.id); // Send message await client.sendMessage({ recipient: 'username', content: 'Hello!' }); ``` ## Common Patterns ### Retry Logic **Python** ```python for attempt in range(3): try: result = await client.create_search(query) return result except TimeoutError: if attempt < 2: await asyncio.sleep(2 ** attempt) ``` **Go** ```go for attempt := 0; attempt < 3; attempt++ { result, err := client.CreateSearch(ctx, query) if err == nil { return result, nil } if attempt < 2 { time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second) } } ``` ### Pagination **Python** ```python limit = 50 offset = 0 all_results = [] while True: details = await client.get_search_details(search_id, limit=limit, offset=offset) page = details.get("results", []) if not page: break all_results.extend(page) if len(page) < limit: break offset += len(page) ``` **Go** ```go limit := 50 offset := 0 total := 0 for { details, err := client.GetSearchDetails(ctx, searchID, limit, offset) if err != nil { break } page, ok := details["results"].([]interface{}) if !ok || len(page) == 0 { break } total += len(page) if len(page) < limit { break } offset += len(page) } ``` ### Concurrent Operations **Python** ```python tasks = [client.create_search(q) for q in queries] results = await asyncio.gather(*tasks, return_exceptions=True) ``` **Go** ```go results := make(chan map[string]interface{}, len(queries)) var wg sync.WaitGroup for _, q := range queries { wg.Add(1) go func(query string) { defer wg.Done() result, _ := client.CreateSearch(ctx, query) results <- result }(q) } ``` ## Error Handling ### Python ```python from slskr import ApiError, NetworkError, TimeoutError try: await client.create_search(query) except ApiError as e: if e.is_unauthorized(): # 401 - Invalid token elif e.is_not_found(): # 404 - Endpoint not found elif e.is_server_error(): # 5xx - Server error except TimeoutError: # Request timed out except NetworkError as e: # Network connectivity issue ``` ### Go ```go import "net/http" result, err := client.CreateSearch(ctx, query) if err != nil { // Handle based on error type if strings.Contains(err.Error(), "401") { // Unauthorized } else if strings.Contains(err.Error(), "404") { // Not found } else if strings.Contains(err.Error(), "5") { // Server error } } ``` ## Performance Considerations ### Batch Operations Use batch operations to reduce request overhead: ```python # Inefficient: 3 separate requests await client.get_stats() await client.get_config() await client.get_capabilities() # Efficient: 1 batch request batch = client.batch.builder() batch.get("/api/stats") batch.get("/api/config") batch.get("/api/capabilities") response = await batch.execute() ``` ### Connection Pooling Python automatically pools connections via aiohttp. Go's http.Client also pools connections. ### Rate Limiting Respect API rate limits: ```python # Automatic retry with exponential backoff client = SlskrClient(..., retries=3, retry_delay=1) # Manual rate limiting import asyncio semaphore = asyncio.Semaphore(5) async def limited_search(query): async with semaphore: return await client.create_search(query) ``` ## Testing ### Python ```bash cd client-python python -m pytest tests/ python -m py_compile examples/*.py ``` ### Go ```bash cd client-go go test ./... for example in examples/*.go; do go test -tags examples "./$example" || exit 1; done ``` ### TypeScript ```bash cd client-ts npm test -- --runInBand npm run build npx tsc --ignoreConfig --strict --target ES2020 --module Node16 \ --moduleResolution Node16 --esModuleInterop --skipLibCheck \ --ignoreDeprecations 6.0 --noEmit examples/basic-usage.ts ``` ## Contributing Client libraries are maintained in: - `client-python/` - Python async client - `client-go/` - Go client - `client-ts/` - TypeScript/JavaScript client ## Support - **Issues**: GitHub Issues - **Documentation**: See `docs/` directory - **Examples**: See `examples/` directories in each client - **API Reference**: See `docs/openapi.json` ## License Same as slskr main project