mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
feat: implement /introspect and /recents endpoints with full CLI support
🔥 NEW ENDPOINTS IMPLEMENTED: 📊 /introspect endpoint: - Get detailed music service state and capabilities data - Support for SPOTIFY, PANDORA, TUNEIN, AMAZON, DEEZER services - Service state tracking (Active, Inactive, InactiveUnselected) - Playback capabilities (skip, seek, resume, data collection) - Authentication token status and user account information - Subscription type and content history metadata 📚 /recents endpoint: - Retrieve recently played content history - Support for all music sources (Spotify, Local, TuneIn, Pandora, etc.) - Rich filtering by source type and content type - Content classification (tracks, stations, playlists, albums) - Presetable item identification and artwork metadata - Timestamp tracking with UTC time support ⚡ CLIENT API: - client.Introspect(source, sourceAccount) method - client.IntrospectSpotify(sourceAccount) convenience method - client.GetRecents() method with comprehensive filtering - Complete error handling and validation - Rich helper methods for content analysis 🖥️ CLI COMMANDS: - soundtouch-cli source introspect --source <SERVICE> - soundtouch-cli source introspect-spotify - soundtouch-cli source introspect-all (bulk introspect) - soundtouch-cli recents list [--detailed] [--limit N] - soundtouch-cli recents filter --source <SRC> --type <TYPE> - soundtouch-cli recents latest (most recent item) - soundtouch-cli recents stats (detailed analytics) 📦 MODELS & FEATURES: - IntrospectRequest/Response with service-specific handling - RecentsResponse with RecentsResponseItem for individual items - Rich filtering: GetSpotifyItems(), GetTracks(), GetPresetableItems() - Content type detection: IsTrack(), IsStation(), IsPlaylist() - Source classification: IsStreamingContent(), IsLocalContent() - Full XML marshalling/unmarshalling with proper attribute handling 🧪 COMPREHENSIVE TESTING: - Unit tests for models with XML parsing validation - Integration tests for real device communication - CLI command tests with mock server responses - Error condition testing and edge case handling - Performance tests and timeout validation 📖 DOCUMENTATION & EXAMPLES: - Updated API endpoints overview marking endpoints as implemented - Comprehensive CLI reference with usage examples - Removed endpoints from unimplemented list - Updated wiki implementation plan status - Complete example applications with README guides - Real-world usage patterns and best practices ✨ KEY FEATURES: - Service health monitoring and diagnostics - Recently played content discovery and analysis - Preset candidate identification - Content statistics and usage analytics - Time-based filtering and relative timestamps - Rich emoji-based CLI output formatting - Cross-service compatibility and error handling This implements two critical missing endpoints from the SoundTouch API, providing essential functionality for music service management and recently played content analysis with full programmatic and CLI access.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# Introspect Endpoint Example
|
||||
|
||||
This example demonstrates how to use the `/introspect` endpoint to get detailed information about music service states and capabilities on your SoundTouch device.
|
||||
|
||||
## What is the Introspect Endpoint?
|
||||
|
||||
The introspect endpoint provides detailed information about music services (like Spotify, Pandora, TuneIn) including:
|
||||
|
||||
- **Service State**: Active, Inactive, or InactiveUnselected
|
||||
- **User Information**: Associated account names
|
||||
- **Playback Status**: Currently playing content and URIs
|
||||
- **Service Capabilities**: Skip, seek, resume support
|
||||
- **Token Information**: Authentication token status
|
||||
- **Content History**: History size limits
|
||||
- **Subscription Details**: Premium/free account status
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage - check Spotify status
|
||||
go run main.go -host 192.168.1.100
|
||||
|
||||
# Check specific service with account
|
||||
go run main.go -host 192.168.1.100 -source SPOTIFY -account "your_spotify_username"
|
||||
|
||||
# Check Pandora service
|
||||
go run main.go -host 192.168.1.100 -source PANDORA
|
||||
|
||||
# Check TuneIn radio
|
||||
go run main.go -host 192.168.1.100 -source TUNEIN
|
||||
|
||||
# Custom timeout
|
||||
go run main.go -host 192.168.1.100 -timeout 5s
|
||||
```
|
||||
|
||||
## Command Line Options
|
||||
|
||||
- `-host` - **Required**: SoundTouch device IP address
|
||||
- `-source` - Music service to introspect (default: `SPOTIFY`)
|
||||
- Supported: `SPOTIFY`, `PANDORA`, `TUNEIN`, `AMAZON`, `DEEZER`, etc.
|
||||
- `-account` - Source account name (optional)
|
||||
- `-timeout` - Request timeout (default: `10s`)
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
Getting introspect data for SPOTIFY
|
||||
|
||||
=== SPOTIFY Service Introspect Data ===
|
||||
State: InactiveUnselected
|
||||
User: SpotifyConnectUserName
|
||||
Currently Playing: false
|
||||
Current Content:
|
||||
Shuffle Mode: OFF
|
||||
Subscription Type:
|
||||
|
||||
=== Service State ===
|
||||
❌ Service is INACTIVE
|
||||
|
||||
=== Service Capabilities ===
|
||||
❌ Skip Previous not supported
|
||||
❌ Seek not supported
|
||||
✅ Resume supported
|
||||
✅ Data collection enabled
|
||||
|
||||
=== Content History ===
|
||||
Max History Size: 10 items
|
||||
|
||||
=== Technical Details ===
|
||||
Token Last Changed: 1702566495 seconds
|
||||
Token Microseconds: 427884
|
||||
Play Status State: 2
|
||||
Received Playback Request: false
|
||||
|
||||
=== Service Availability Check ===
|
||||
✅ Spotify is available on this device
|
||||
|
||||
Done!
|
||||
```
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
### Service States
|
||||
- **Active**: Service is currently selected and active
|
||||
- **Inactive**: Service is available but not currently active
|
||||
- **InactiveUnselected**: Service is available but never been used
|
||||
|
||||
### Capabilities
|
||||
- **Skip Previous**: Can skip to previous track
|
||||
- **Seek**: Can seek within tracks (scrub timeline)
|
||||
- **Resume**: Can resume paused playback
|
||||
- **Data Collection**: Service collects usage analytics
|
||||
|
||||
### Technical Fields
|
||||
- **Token Last Changed**: Unix timestamp of last authentication
|
||||
- **Play Status State**: Internal playback state code
|
||||
- **Current URI**: Unique identifier for currently playing content
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Check if Spotify is Logged In
|
||||
```go
|
||||
response, err := client.Introspect("SPOTIFY", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if response.HasUser() && response.IsActive() {
|
||||
fmt.Println("Spotify is logged in and active")
|
||||
} else {
|
||||
fmt.Println("Spotify needs authentication or activation")
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Verify Service Capabilities Before Playback Control
|
||||
```go
|
||||
response, err := client.IntrospectSpotify("")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if response.SupportsSeek() {
|
||||
// Safe to use seek controls
|
||||
fmt.Println("Seek controls available")
|
||||
}
|
||||
|
||||
if response.SupportsSkipPrevious() {
|
||||
// Safe to use previous track
|
||||
fmt.Println("Previous track control available")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Monitor Service Health
|
||||
```go
|
||||
response, err := client.Introspect("PANDORA", "my_pandora_user")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !response.IsActive() {
|
||||
fmt.Println("Pandora service needs activation")
|
||||
}
|
||||
|
||||
if response.HasSubscription() {
|
||||
fmt.Printf("Premium account: %s\n", response.SubscriptionType)
|
||||
}
|
||||
```
|
||||
|
||||
## Related API Methods
|
||||
|
||||
- `client.GetServiceAvailability()` - Check which services are available
|
||||
- `client.SelectSource(source, account)` - Activate a music service
|
||||
- `client.GetNowPlaying()` - Get current playback information
|
||||
|
||||
## Error Handling
|
||||
|
||||
The introspect endpoint may fail if:
|
||||
- Service is not supported on the device
|
||||
- Invalid source name provided
|
||||
- Network connectivity issues
|
||||
- Device is in standby mode
|
||||
|
||||
Always check for errors and handle gracefully:
|
||||
|
||||
```go
|
||||
response, err := client.Introspect("SPOTIFY", "")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "failed to get introspect data") {
|
||||
fmt.Println("Service may not be configured or available")
|
||||
return
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Other Examples
|
||||
|
||||
This introspect data is useful before:
|
||||
- [Preset Management](../preset-management/) - Verify service state before storing presets
|
||||
- [Source Selection](../source-selection/) - Check capabilities before switching sources
|
||||
- [Zone Management](../zone-management/) - Ensure all devices support the service
|
||||
|
||||
## API Documentation
|
||||
|
||||
For complete API documentation, see:
|
||||
- [API Reference](../../docs/API-Endpoints-Overview.md)
|
||||
- [Service Management Guide](../../docs/SERVICE-MANAGEMENT.md)
|
||||
@@ -0,0 +1,393 @@
|
||||
# Introspect CLI Commands Demo
|
||||
|
||||
This document demonstrates the usage and output of the new introspect CLI commands added to the soundtouch-cli tool.
|
||||
|
||||
## Available Commands
|
||||
|
||||
The introspect functionality is available through three commands in the `source` command group:
|
||||
|
||||
1. `source introspect` - Get introspect data for any supported service
|
||||
2. `source introspect-spotify` - Convenience command specifically for Spotify
|
||||
3. `source introspect-all` - Get introspect data for all available services
|
||||
|
||||
## Command Examples and Expected Output
|
||||
|
||||
### 1. Basic Spotify Introspect
|
||||
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect --source SPOTIFY
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
Getting introspect data for SPOTIFY
|
||||
|
||||
=== SPOTIFY Service Introspect Data ===
|
||||
State: InactiveUnselected
|
||||
User: SpotifyConnectUserName
|
||||
Currently Playing: ❌ No
|
||||
Current Content:
|
||||
Shuffle Mode: OFF
|
||||
Subscription Type:
|
||||
|
||||
=== Service State ===
|
||||
❌ Service is INACTIVE (Never been used)
|
||||
⏸️ Not currently playing
|
||||
➡️ Shuffle mode is OFF
|
||||
|
||||
=== Service Capabilities ===
|
||||
❌ ⏮️ Skip Previous
|
||||
❌ 🎯 Seek within tracks
|
||||
✅ ▶️ Resume playback
|
||||
✅ 📊 Data collection: ENABLED
|
||||
|
||||
=== Spotify Content History ===
|
||||
Max History Size: 10 items
|
||||
|
||||
=== Technical Details ===
|
||||
Token Last Changed: 2023-12-14 10:48:15 MST
|
||||
Token Timestamp: 1702566495 seconds since Unix epoch
|
||||
Token Microseconds: 427884
|
||||
Play Status State: 2
|
||||
Received Playback Request: ❌ No
|
||||
```
|
||||
|
||||
### 2. Spotify Introspect with Account
|
||||
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect --source SPOTIFY --account my_spotify_user
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
Getting introspect data for SPOTIFY
|
||||
Source Account: my_spotify_user
|
||||
|
||||
=== SPOTIFY Service Introspect Data ===
|
||||
State: Active
|
||||
User: my_spotify_user
|
||||
Currently Playing: ✅ Yes
|
||||
Current Content: spotify://track/4iV5W9uYEdYUVa79Axb7Rh
|
||||
Shuffle Mode: ON
|
||||
Subscription Type: Premium
|
||||
|
||||
=== Service State ===
|
||||
✅ Service is ACTIVE
|
||||
🎵 Currently playing content
|
||||
🔀 Shuffle mode is ON
|
||||
|
||||
=== Service Capabilities ===
|
||||
✅ ⏮️ Skip Previous
|
||||
✅ 🎯 Seek within tracks
|
||||
✅ ▶️ Resume playback
|
||||
🚫 Data collection: DISABLED
|
||||
|
||||
=== Spotify Content History ===
|
||||
Max History Size: 15 items
|
||||
|
||||
=== Technical Details ===
|
||||
Token Last Changed: 2023-12-14 15:30:22 MST
|
||||
Token Timestamp: 1702583422 seconds since Unix epoch
|
||||
Token Microseconds: 123456
|
||||
Play Status State: 1
|
||||
Received Playback Request: ✅ Yes
|
||||
```
|
||||
|
||||
### 3. Spotify Convenience Command
|
||||
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect-spotify
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
Getting Spotify introspect data
|
||||
|
||||
=== Spotify Service Introspect Data ===
|
||||
State: Active
|
||||
User: premium_user
|
||||
Currently Playing: ✅ Yes
|
||||
Current Content: spotify://playlist/37i9dQZF1DXcBWIGoYBM5M
|
||||
Shuffle Mode: ON
|
||||
Subscription Type: Premium
|
||||
|
||||
=== Spotify Service State ===
|
||||
✅ Service is ACTIVE
|
||||
🎵 Currently playing content
|
||||
🔀 Shuffle mode is ON
|
||||
|
||||
=== Spotify Service Capabilities ===
|
||||
✅ ⏮️ Skip Previous
|
||||
✅ 🎯 Seek within tracks
|
||||
✅ ▶️ Resume playback
|
||||
🚫 Data collection: DISABLED
|
||||
|
||||
💡 Spotify Setup Recommendations:
|
||||
(None - service is properly configured and active)
|
||||
|
||||
=== Spotify Content History ===
|
||||
Max History Size: 20 items
|
||||
|
||||
=== Technical Details ===
|
||||
Token Last Changed: 2023-12-14 16:45:10 MST
|
||||
Token Timestamp: 1702587910 seconds since Unix epoch
|
||||
Token Microseconds: 789012
|
||||
Play Status State: 1
|
||||
Received Playback Request: ✅ Yes
|
||||
```
|
||||
|
||||
### 4. Inactive Service Example
|
||||
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect-spotify
|
||||
```
|
||||
|
||||
**Expected Output (when Spotify is not set up):**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
Getting Spotify introspect data
|
||||
|
||||
=== Spotify Service Introspect Data ===
|
||||
State: InactiveUnselected
|
||||
User:
|
||||
Currently Playing: ❌ No
|
||||
Current Content:
|
||||
Shuffle Mode: OFF
|
||||
Subscription Type:
|
||||
|
||||
=== Spotify Service State ===
|
||||
❌ Service is INACTIVE (Never been used)
|
||||
⏸️ Not currently playing
|
||||
➡️ Shuffle mode is OFF
|
||||
|
||||
=== Spotify Service Capabilities ===
|
||||
❌ ⏮️ Skip Previous
|
||||
❌ 🎯 Seek within tracks
|
||||
✅ ▶️ Resume playback
|
||||
✅ 📊 Data collection: ENABLED
|
||||
|
||||
💡 Spotify Setup Recommendations:
|
||||
• Sign in to your Spotify account on the device
|
||||
• Use 'soundtouch-cli source select --source SPOTIFY' to activate Spotify
|
||||
• Ensure you have Spotify Premium for full functionality
|
||||
```
|
||||
|
||||
### 5. All Services Introspect
|
||||
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect-all
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
Getting introspect data for all services
|
||||
|
||||
🔍 Getting introspect data for SPOTIFY...
|
||||
✅ SPOTIFY: Successfully retrieved introspect data
|
||||
State: Active (User: spotify_user)
|
||||
Playing: ✅ Yes | Content: spotify://track/4iV5W9uYEdYUVa79Axb7Rh
|
||||
Capabilities: Skip, Seek, Resume
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
🔍 Getting introspect data for PANDORA...
|
||||
❌ PANDORA: Service not available on this device
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
🔍 Getting introspect data for TUNEIN...
|
||||
✅ TUNEIN: Successfully retrieved introspect data
|
||||
State: Inactive
|
||||
Playing: ❌ No
|
||||
Capabilities: Resume
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
🔍 Getting introspect data for AMAZON...
|
||||
❌ AMAZON: Failed to get introspect data - service not configured
|
||||
|
||||
──────────────────────────────────────────────────
|
||||
🔍 Getting introspect data for DEEZER...
|
||||
❌ DEEZER: Service not available on this device
|
||||
|
||||
══════════════════════════════════════════════════
|
||||
📊 Introspect Summary:
|
||||
✅ Successful: 2 services
|
||||
❌ Failed: 3 services
|
||||
📡 Total checked: 5 services
|
||||
|
||||
✅ Successfully retrieved introspect data for 2 services
|
||||
```
|
||||
|
||||
### 6. Error Handling Examples
|
||||
|
||||
#### Missing Source Parameter
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
NAME:
|
||||
soundtouch-cli source introspect - Get introspect data for a music service
|
||||
|
||||
USAGE:
|
||||
soundtouch-cli source introspect [command options]
|
||||
|
||||
OPTIONS:
|
||||
--account value, -a value Source account name (optional)
|
||||
--source value, -s value Music service source (SPOTIFY, PANDORA, TUNEIN, etc.)
|
||||
--help, -h show help
|
||||
|
||||
Required flag "source" not set
|
||||
```
|
||||
|
||||
#### Missing Host Parameter
|
||||
```bash
|
||||
$ soundtouch-cli source introspect --source SPOTIFY
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
host is required. Use --host flag or set SOUNDTOUCH_HOST environment variable
|
||||
```
|
||||
|
||||
#### Invalid Service
|
||||
```bash
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect --source INVALID_SERVICE
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ SoundTouch CLI v1.0.0
|
||||
🔗 Connecting to SoundTouch device at 192.168.1.100:8090
|
||||
|
||||
⚠️ Service INVALID_SERVICE may not be available, but continuing with introspect request...
|
||||
|
||||
Getting introspect data for INVALID_SERVICE
|
||||
|
||||
❌ Error: failed to get introspect data: HTTP 404: endpoint not found or service not supported
|
||||
```
|
||||
|
||||
## Integration with Other Commands
|
||||
|
||||
The introspect commands work well with other CLI commands:
|
||||
|
||||
### 1. Check Availability First
|
||||
```bash
|
||||
# Check what services are available
|
||||
$ soundtouch-cli --host 192.168.1.100 source availability
|
||||
|
||||
# Then introspect specific services
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect --source SPOTIFY
|
||||
```
|
||||
|
||||
### 2. Activate Service After Introspect
|
||||
```bash
|
||||
# Check service status
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect-spotify
|
||||
|
||||
# If inactive, activate it
|
||||
$ soundtouch-cli --host 192.168.1.100 source select --source SPOTIFY
|
||||
```
|
||||
|
||||
### 3. Compare Sources and Introspect Data
|
||||
```bash
|
||||
# Compare configured sources vs available services
|
||||
$ soundtouch-cli --host 192.168.1.100 source compare
|
||||
|
||||
# Get detailed introspect data for specific services
|
||||
$ soundtouch-cli --host 192.168.1.100 source introspect-all
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The introspect commands respect the same environment variables as other CLI commands:
|
||||
|
||||
- `SOUNDTOUCH_HOST` - Default device IP address
|
||||
- `SOUNDTOUCH_SKIP_AVAILABILITY_CHECK` - Skip service availability validation
|
||||
- `SOUNDTOUCH_TIMEOUT` - Request timeout duration
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
export SOUNDTOUCH_HOST=192.168.1.100
|
||||
soundtouch-cli source introspect-spotify
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Service Setup Verification
|
||||
Check if streaming services are properly configured and authenticated:
|
||||
```bash
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify
|
||||
soundtouch-cli --host $DEVICE source introspect --source PANDORA
|
||||
```
|
||||
|
||||
### 2. Troubleshooting Playback Issues
|
||||
Understand why certain playback controls aren't working:
|
||||
```bash
|
||||
# Check if seek is supported
|
||||
soundtouch-cli --host $DEVICE source introspect --source SPOTIFY | grep -i seek
|
||||
|
||||
# Check current playback state
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify | grep -i playing
|
||||
```
|
||||
|
||||
### 3. Service Health Monitoring
|
||||
Monitor the health and status of streaming services:
|
||||
```bash
|
||||
# Quick health check for all services
|
||||
soundtouch-cli --host $DEVICE source introspect-all
|
||||
|
||||
# Detailed status for critical service
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify
|
||||
```
|
||||
|
||||
### 4. Account Management
|
||||
Verify which accounts are associated with services:
|
||||
```bash
|
||||
# Check current Spotify account
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify | grep -i user
|
||||
|
||||
# Check with specific account parameter
|
||||
soundtouch-cli --host $DEVICE source introspect --source SPOTIFY --account specific_user
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use with grep**: Pipe output to `grep` to filter specific information:
|
||||
```bash
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify | grep -E "(State|User|Playing)"
|
||||
```
|
||||
|
||||
2. **JSON output**: While not currently implemented, future versions may support JSON output for scripting:
|
||||
```bash
|
||||
# Future feature
|
||||
soundtouch-cli --host $DEVICE source introspect-spotify --format json
|
||||
```
|
||||
|
||||
3. **Batch operations**: Use shell scripting to check multiple devices:
|
||||
```bash
|
||||
for device in 192.168.1.100 192.168.1.101; do
|
||||
echo "=== Device $device ==="
|
||||
soundtouch-cli --host $device source introspect-spotify
|
||||
done
|
||||
```
|
||||
|
||||
4. **Environment setup**: Set up your environment for easier usage:
|
||||
```bash
|
||||
export SOUNDTOUCH_HOST=192.168.1.100
|
||||
alias st='soundtouch-cli'
|
||||
st source introspect-spotify
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
host = flag.String("host", "", "SoundTouch device IP address")
|
||||
source = flag.String("source", "SPOTIFY", "Music service source (SPOTIFY, PANDORA, TUNEIN)")
|
||||
sourceAccount = flag.String("account", "", "Source account name (optional)")
|
||||
timeout = flag.Duration("timeout", 10*time.Second, "Request timeout")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *host == "" {
|
||||
log.Fatal("Please provide a SoundTouch device IP address with -host flag")
|
||||
}
|
||||
|
||||
// Create client
|
||||
config := &client.Config{
|
||||
Host: *host,
|
||||
Port: 8090,
|
||||
Timeout: *timeout,
|
||||
}
|
||||
soundTouchClient := client.NewClient(config)
|
||||
|
||||
fmt.Printf("Getting introspect data for %s", *source)
|
||||
if *sourceAccount != "" {
|
||||
fmt.Printf(" (account: %s)", *sourceAccount)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Get introspect data
|
||||
response, err := soundTouchClient.Introspect(*source, *sourceAccount)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get introspect data: %v", err)
|
||||
}
|
||||
|
||||
// Display basic information
|
||||
fmt.Printf("\n=== %s Service Introspect Data ===\n", *source)
|
||||
fmt.Printf("State: %s\n", response.State)
|
||||
|
||||
if response.HasUser() {
|
||||
fmt.Printf("User: %s\n", response.User)
|
||||
}
|
||||
|
||||
fmt.Printf("Currently Playing: %t\n", response.IsPlaying)
|
||||
|
||||
if response.HasCurrentContent() {
|
||||
fmt.Printf("Current Content: %s\n", response.CurrentURI)
|
||||
}
|
||||
|
||||
fmt.Printf("Shuffle Mode: %s\n", response.ShuffleMode)
|
||||
|
||||
if response.HasSubscription() {
|
||||
fmt.Printf("Subscription Type: %s\n", response.SubscriptionType)
|
||||
}
|
||||
|
||||
// Display service state
|
||||
fmt.Printf("\n=== Service State ===\n")
|
||||
if response.IsActive() {
|
||||
fmt.Println("✅ Service is ACTIVE")
|
||||
} else if response.IsInactive() {
|
||||
fmt.Println("❌ Service is INACTIVE")
|
||||
}
|
||||
|
||||
// Display capabilities
|
||||
fmt.Printf("\n=== Service Capabilities ===\n")
|
||||
if response.SupportsSkipPrevious() {
|
||||
fmt.Println("✅ Skip Previous supported")
|
||||
} else {
|
||||
fmt.Println("❌ Skip Previous not supported")
|
||||
}
|
||||
|
||||
if response.SupportsSeek() {
|
||||
fmt.Println("✅ Seek supported")
|
||||
} else {
|
||||
fmt.Println("❌ Seek not supported")
|
||||
}
|
||||
|
||||
if response.SupportsResume() {
|
||||
fmt.Println("✅ Resume supported")
|
||||
} else {
|
||||
fmt.Println("❌ Resume not supported")
|
||||
}
|
||||
|
||||
if response.CollectsData() {
|
||||
fmt.Println("📊 Data collection enabled")
|
||||
} else {
|
||||
fmt.Println("🚫 Data collection disabled")
|
||||
}
|
||||
|
||||
// Display history information
|
||||
historySize := response.GetMaxHistorySize()
|
||||
if historySize > 0 {
|
||||
fmt.Printf("\n=== Content History ===\n")
|
||||
fmt.Printf("Max History Size: %d items\n", historySize)
|
||||
}
|
||||
|
||||
// Display technical details
|
||||
if response.TokenLastChangedTimeSeconds > 0 {
|
||||
fmt.Printf("\n=== Technical Details ===\n")
|
||||
fmt.Printf("Token Last Changed: %d seconds\n", response.TokenLastChangedTimeSeconds)
|
||||
if response.TokenLastChangedTimeMicroseconds > 0 {
|
||||
fmt.Printf("Token Microseconds: %d\n", response.TokenLastChangedTimeMicroseconds)
|
||||
}
|
||||
fmt.Printf("Play Status State: %s\n", response.PlayStatusState)
|
||||
fmt.Printf("Received Playback Request: %t\n", response.ReceivedPlaybackRequest)
|
||||
}
|
||||
|
||||
// Show service availability for comparison
|
||||
fmt.Printf("\n=== Service Availability Check ===\n")
|
||||
availability, err := soundTouchClient.GetServiceAvailability()
|
||||
if err != nil {
|
||||
fmt.Printf("Could not check service availability: %v\n", err)
|
||||
} else {
|
||||
switch *source {
|
||||
case "SPOTIFY":
|
||||
if availability.HasSpotify() {
|
||||
fmt.Println("✅ Spotify is available on this device")
|
||||
} else {
|
||||
fmt.Println("❌ Spotify is not available on this device")
|
||||
}
|
||||
case "PANDORA":
|
||||
if availability.HasPandora() {
|
||||
fmt.Println("✅ Pandora is available on this device")
|
||||
} else {
|
||||
fmt.Println("❌ Pandora is not available on this device")
|
||||
}
|
||||
case "TUNEIN":
|
||||
if availability.HasTuneIn() {
|
||||
fmt.Println("✅ TuneIn is available on this device")
|
||||
} else {
|
||||
fmt.Println("❌ TuneIn is not available on this device")
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Service availability check not implemented for %s\n", *source)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nDone!")
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
# Recents Endpoint Example
|
||||
|
||||
This example demonstrates how to use the `/recents` endpoint to retrieve and analyze recently played content from your SoundTouch device.
|
||||
|
||||
## What is the Recents Endpoint?
|
||||
|
||||
The recents endpoint provides access to the device's recently played content history, including:
|
||||
|
||||
- **Recently played tracks** from various music services
|
||||
- **Radio stations** that were recently listened to
|
||||
- **Playlists and albums** that were recently accessed
|
||||
- **Local music** files that were recently played
|
||||
- **Metadata** including play timestamps, content types, and source information
|
||||
- **Filtering capabilities** by source type and content type
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage - show last 10 items
|
||||
go run main.go -host 192.168.1.100
|
||||
|
||||
# Show detailed information for all items
|
||||
go run main.go -host 192.168.1.100 -detailed -limit 0
|
||||
|
||||
# Filter by source (show only Spotify items)
|
||||
go run main.go -host 192.168.1.100 -source SPOTIFY
|
||||
|
||||
# Filter by content type (show only tracks)
|
||||
go run main.go -host 192.168.1.100 -type track
|
||||
|
||||
# Show statistics only
|
||||
go run main.go -host 192.168.1.100 -stats
|
||||
|
||||
# Combined filters with custom limit
|
||||
go run main.go -host 192.168.1.100 -source LOCAL_MUSIC -type track -limit 5 -detailed
|
||||
```
|
||||
|
||||
## Command Line Options
|
||||
|
||||
- `-host` - **Required**: SoundTouch device IP address
|
||||
- `-detailed` - Show detailed information for each item (default: false)
|
||||
- `-limit` - Maximum number of items to display, 0 for all (default: 10)
|
||||
- `-source` - Filter by source (SPOTIFY, LOCAL_MUSIC, TUNEIN, etc.)
|
||||
- `-type` - Filter by content type (track, station, playlist, album, presetable)
|
||||
- `-stats` - Show statistics only (default: false)
|
||||
- `-timeout` - Request timeout duration (default: 10s)
|
||||
|
||||
## Example Output
|
||||
|
||||
### Basic Listing
|
||||
```
|
||||
Getting recent items from 192.168.1.100
|
||||
|
||||
📊 Recent Items Summary:
|
||||
Showing: 5 items (of 15 total)
|
||||
By Source: Spotify: 3, Local: 1, TuneIn: 1
|
||||
|
||||
=== Recent Items ===
|
||||
1. 🎵 Shape of You - Ed Sheeran
|
||||
Source: Spotify | Type: Track
|
||||
Played: 2023-12-14 15:30:22 (2 hours ago)
|
||||
|
||||
2. 📻 BBC Radio 1
|
||||
Source: TuneIn Radio | Type: Stationurl
|
||||
Played: 2023-12-14 13:15:45 (4 hours ago)
|
||||
|
||||
3. 🎵 Local Song.mp3
|
||||
Source: Local Music | Type: Track
|
||||
Played: 2023-12-14 10:45:12 (7 hours ago)
|
||||
|
||||
💡 Showing 3 of 15 total items
|
||||
Use -limit 0 to show all items
|
||||
```
|
||||
|
||||
### Detailed Information
|
||||
```
|
||||
1. 🎵 Shape of You - Ed Sheeran
|
||||
Source: Spotify | Type: Track
|
||||
Played: 2023-12-14 15:30:22 (2 hours ago)
|
||||
ID: spotify123
|
||||
⭐ Can be saved as preset
|
||||
🎨 Has artwork
|
||||
📍 Location: spotify:track:4iV5W9uYEdYUVa79Axb7Rh
|
||||
👤 Account: spotify_user
|
||||
🏷️ Type: Streaming
|
||||
```
|
||||
|
||||
### Statistics View
|
||||
```
|
||||
📊 Recent Items Statistics
|
||||
|
||||
Overall Statistics:
|
||||
Total Items: 25
|
||||
Last Played: 2023-12-14 15:30:22
|
||||
|
||||
📍 By Source:
|
||||
Spotify 15 items ( 60.0%)
|
||||
Local Music 6 items ( 24.0%)
|
||||
TuneIn 3 items ( 12.0%)
|
||||
Pandora 1 items ( 4.0%)
|
||||
|
||||
🎼 By Content Type:
|
||||
Tracks 20 items ( 80.0%)
|
||||
Stations 4 items ( 16.0%)
|
||||
Playlists/Albums 1 items ( 4.0%)
|
||||
|
||||
⭐ Special Categories:
|
||||
Presetable 18 items ( 72.0%)
|
||||
|
||||
📡 Source Analysis:
|
||||
Streaming 19 items ( 76.0%)
|
||||
Local 6 items ( 24.0%)
|
||||
|
||||
🕐 Time Analysis:
|
||||
Today 12 items
|
||||
Yesterday 8 items
|
||||
This Week 3 items
|
||||
Older 2 items
|
||||
```
|
||||
|
||||
## Supported Sources
|
||||
|
||||
- **SPOTIFY** - Spotify streaming service
|
||||
- **LOCAL_MUSIC** - Local music files
|
||||
- **STORED_MUSIC** - Stored music library
|
||||
- **TUNEIN** - TuneIn radio stations
|
||||
- **PANDORA** - Pandora music service
|
||||
- **AMAZON** - Amazon Music
|
||||
- **DEEZER** - Deezer streaming
|
||||
- **IHEART** - iHeartRadio
|
||||
- **BLUETOOTH** - Bluetooth input
|
||||
- **AUX** - AUX input
|
||||
- **AIRPLAY** - AirPlay
|
||||
|
||||
## Content Types
|
||||
|
||||
- **track** - Individual songs/tracks
|
||||
- **station** - Radio stations
|
||||
- **playlist** - Music playlists
|
||||
- **album** - Music albums
|
||||
- **container** - Folders/collections
|
||||
- **presetable** - Items that can be saved as presets
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Recently Played Music Discovery
|
||||
```bash
|
||||
# Find recently played Spotify tracks
|
||||
go run main.go -host 192.168.1.100 -source SPOTIFY -type track -detailed
|
||||
```
|
||||
|
||||
### 2. Radio Station History
|
||||
```bash
|
||||
# See what radio stations were recently played
|
||||
go run main.go -host 192.168.1.100 -type station -detailed
|
||||
```
|
||||
|
||||
### 3. Content Analytics
|
||||
```bash
|
||||
# Get detailed listening statistics
|
||||
go run main.go -host 192.168.1.100 -stats
|
||||
```
|
||||
|
||||
### 4. Preset Candidates
|
||||
```bash
|
||||
# Find content that can be saved as presets
|
||||
go run main.go -host 192.168.1.100 -type presetable -limit 6
|
||||
```
|
||||
|
||||
### 5. Local vs Streaming Analysis
|
||||
```bash
|
||||
# Compare local vs streaming content usage
|
||||
go run main.go -host 192.168.1.100 -stats
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
The example demonstrates several key API patterns:
|
||||
|
||||
### Basic Retrieval
|
||||
```go
|
||||
response, err := client.GetRecents()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if response.IsEmpty() {
|
||||
fmt.Println("No recent items found")
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering by Source
|
||||
```go
|
||||
spotifyItems := response.GetSpotifyItems()
|
||||
localItems := response.GetLocalMusicItems()
|
||||
tuneInItems := response.GetTuneInItems()
|
||||
```
|
||||
|
||||
### Filtering by Type
|
||||
```go
|
||||
tracks := response.GetTracks()
|
||||
stations := response.GetStations()
|
||||
presetableItems := response.GetPresetableItems()
|
||||
```
|
||||
|
||||
### Item Analysis
|
||||
```go
|
||||
for _, item := range response.Items {
|
||||
if item.IsSpotifyContent() {
|
||||
fmt.Printf("Spotify track: %s\n", item.GetDisplayName())
|
||||
}
|
||||
|
||||
if item.IsPresetable() {
|
||||
fmt.Printf("Can be saved as preset: %s\n", item.GetDisplayName())
|
||||
}
|
||||
|
||||
if item.HasArtwork() {
|
||||
fmt.Printf("Artwork URL: %s\n", item.GetArtwork())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The example includes comprehensive error handling:
|
||||
|
||||
```bash
|
||||
# Test with invalid host
|
||||
go run main.go -host 192.168.255.255
|
||||
# Output: Failed to get recent items: connection timeout
|
||||
|
||||
# Test with unknown source
|
||||
go run main.go -host 192.168.1.100 -source UNKNOWN
|
||||
# Output: 📭 No items found for source: UNKNOWN
|
||||
# 💡 Available sources: SPOTIFY, LOCAL_MUSIC, TUNEIN
|
||||
|
||||
# Test with unknown type
|
||||
go run main.go -host 192.168.1.100 -type unknown
|
||||
# Output: ❌ Unknown type filter: unknown
|
||||
# 💡 Available types: track, station, playlist, album, presetable
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- The recents endpoint typically returns up to 20-50 items depending on device configuration
|
||||
- Response times are usually under 500ms for typical recent lists
|
||||
- Use filtering to reduce processing time for large recent lists
|
||||
- Consider caching results if calling frequently in applications
|
||||
|
||||
## Integration with Other Examples
|
||||
|
||||
This recents data is useful for:
|
||||
- [Preset Management](../preset-management/) - Finding presetable content to save
|
||||
- [Source Selection](../source-selection/) - Understanding usage patterns
|
||||
- [Navigation](../navigation/) - Quickly accessing recently played content
|
||||
|
||||
## Related CLI Commands
|
||||
|
||||
```bash
|
||||
# List recent items using CLI
|
||||
soundtouch-cli --host 192.168.1.100 recents list
|
||||
|
||||
# Filter recent items by source
|
||||
soundtouch-cli --host 192.168.1.100 recents filter --source SPOTIFY
|
||||
|
||||
# Get recent items statistics
|
||||
soundtouch-cli --host 192.168.1.100 recents stats
|
||||
|
||||
# Show most recent item only
|
||||
soundtouch-cli --host 192.168.1.100 recents latest
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
For complete API documentation, see:
|
||||
- [API Reference](../../docs/API-Endpoints-Overview.md)
|
||||
- [CLI Reference](../../docs/CLI-REFERENCE.md)
|
||||
- [Recents Models](../../pkg/models/recents.go)
|
||||
@@ -0,0 +1,479 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/client"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
host = flag.String("host", "", "SoundTouch device IP address")
|
||||
timeout = flag.Duration("timeout", 10*time.Second, "Request timeout")
|
||||
detailed = flag.Bool("detailed", false, "Show detailed information for each item")
|
||||
limit = flag.Int("limit", 10, "Maximum number of items to display (0 for all)")
|
||||
source = flag.String("source", "", "Filter by source (SPOTIFY, LOCAL_MUSIC, etc.)")
|
||||
itemType = flag.String("type", "", "Filter by type (track, station, playlist, presetable)")
|
||||
stats = flag.Bool("stats", false, "Show statistics only")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *host == "" {
|
||||
log.Fatal("Please provide a SoundTouch device IP address with -host flag")
|
||||
}
|
||||
|
||||
// Create client
|
||||
config := &client.Config{
|
||||
Host: *host,
|
||||
Port: 8090,
|
||||
Timeout: *timeout,
|
||||
}
|
||||
soundTouchClient := client.NewClient(config)
|
||||
|
||||
fmt.Printf("Getting recent items from %s\n", *host)
|
||||
|
||||
// Get recent items
|
||||
response, err := soundTouchClient.GetRecents()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get recent items: %v", err)
|
||||
}
|
||||
|
||||
if response.IsEmpty() {
|
||||
fmt.Println("\n📭 No recent items found")
|
||||
fmt.Println("💡 Play some content to populate the recent items list")
|
||||
return
|
||||
}
|
||||
|
||||
// Show statistics if requested
|
||||
if *stats {
|
||||
showStatistics(response)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
items := response.Items
|
||||
|
||||
if *source != "" {
|
||||
items = response.GetItemsBySource(strings.ToUpper(*source))
|
||||
if len(items) == 0 {
|
||||
fmt.Printf("📭 No items found for source: %s\n", *source)
|
||||
fmt.Println("💡 Available sources:", getAvailableSources(response))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Apply type filter
|
||||
if *itemType != "" {
|
||||
var filteredItems []models.RecentsResponseItem
|
||||
switch strings.ToLower(*itemType) {
|
||||
case "track", "tracks":
|
||||
for _, item := range items {
|
||||
if item.IsTrack() {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
case "station", "stations":
|
||||
for _, item := range items {
|
||||
if item.IsStation() {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
case "playlist", "playlists":
|
||||
for _, item := range items {
|
||||
if item.IsPlaylist() {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
case "album", "albums":
|
||||
for _, item := range items {
|
||||
if item.IsAlbum() {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
case "presetable":
|
||||
for _, item := range items {
|
||||
if item.IsPresetable() {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
default:
|
||||
fmt.Printf("❌ Unknown type filter: %s\n", *itemType)
|
||||
fmt.Println("💡 Available types: track, station, playlist, album, presetable")
|
||||
return
|
||||
}
|
||||
|
||||
items = filteredItems
|
||||
|
||||
if len(items) == 0 {
|
||||
fmt.Printf("📭 No items found for type: %s\n", *itemType)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if *limit > 0 && *limit < len(items) {
|
||||
items = items[:*limit]
|
||||
}
|
||||
|
||||
// Display results
|
||||
displayResults(response, items, *detailed, *source, *itemType)
|
||||
}
|
||||
|
||||
func showStatistics(response *models.RecentsResponse) {
|
||||
fmt.Printf("\n📊 Recent Items Statistics\n\n")
|
||||
|
||||
// Basic stats
|
||||
fmt.Printf("Overall Statistics:\n")
|
||||
fmt.Printf(" Total Items: %d\n", response.GetItemCount())
|
||||
|
||||
if !response.IsEmpty() {
|
||||
mostRecent := response.GetMostRecent()
|
||||
if mostRecent != nil {
|
||||
lastPlayTime := time.Unix(mostRecent.GetUTCTime(), 0)
|
||||
fmt.Printf(" Last Played: %s\n", lastPlayTime.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
// Source breakdown
|
||||
fmt.Printf("\n📍 By Source:\n")
|
||||
sourceStats := map[string]int{
|
||||
"Spotify": len(response.GetSpotifyItems()),
|
||||
"Pandora": len(response.GetPandoraItems()),
|
||||
"TuneIn": len(response.GetTuneInItems()),
|
||||
"Local Music": len(response.GetLocalMusicItems()),
|
||||
"Stored Music": len(response.GetStoredMusicItems()),
|
||||
}
|
||||
|
||||
// Sort sources by count
|
||||
type sourceCount struct {
|
||||
name string
|
||||
count int
|
||||
}
|
||||
var sources []sourceCount
|
||||
for name, count := range sourceStats {
|
||||
if count > 0 {
|
||||
sources = append(sources, sourceCount{name, count})
|
||||
}
|
||||
}
|
||||
sort.Slice(sources, func(i, j int) bool {
|
||||
return sources[i].count > sources[j].count
|
||||
})
|
||||
|
||||
for _, sc := range sources {
|
||||
percentage := float64(sc.count) / float64(response.GetItemCount()) * 100
|
||||
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", sc.name+":", sc.count, percentage)
|
||||
}
|
||||
|
||||
// Content type breakdown
|
||||
fmt.Printf("\n🎼 By Content Type:\n")
|
||||
tracks := len(response.GetTracks())
|
||||
stations := len(response.GetStations())
|
||||
playlists := len(response.GetPlaylistsAndAlbums())
|
||||
|
||||
typeStats := []sourceCount{
|
||||
{"Tracks", tracks},
|
||||
{"Stations", stations},
|
||||
{"Playlists/Albums", playlists},
|
||||
}
|
||||
|
||||
for _, ts := range typeStats {
|
||||
if ts.count > 0 {
|
||||
percentage := float64(ts.count) / float64(response.GetItemCount()) * 100
|
||||
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", ts.name+":", ts.count, percentage)
|
||||
}
|
||||
}
|
||||
|
||||
// Special categories
|
||||
presetable := len(response.GetPresetableItems())
|
||||
if presetable > 0 {
|
||||
fmt.Printf("\n⭐ Special Categories:\n")
|
||||
percentage := float64(presetable) / float64(response.GetItemCount()) * 100
|
||||
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Presetable:", presetable, percentage)
|
||||
}
|
||||
|
||||
// Content source analysis
|
||||
streamingCount := 0
|
||||
localCount := 0
|
||||
for _, item := range response.Items {
|
||||
if item.IsStreamingContent() {
|
||||
streamingCount++
|
||||
} else if item.IsLocalContent() {
|
||||
localCount++
|
||||
}
|
||||
}
|
||||
|
||||
if streamingCount > 0 || localCount > 0 {
|
||||
fmt.Printf("\n📡 Source Analysis:\n")
|
||||
if streamingCount > 0 {
|
||||
percentage := float64(streamingCount) / float64(response.GetItemCount()) * 100
|
||||
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Streaming:", streamingCount, percentage)
|
||||
}
|
||||
if localCount > 0 {
|
||||
percentage := float64(localCount) / float64(response.GetItemCount()) * 100
|
||||
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Local:", localCount, percentage)
|
||||
}
|
||||
}
|
||||
|
||||
// Time analysis - show when items were played
|
||||
fmt.Printf("\n🕐 Time Analysis:\n")
|
||||
now := time.Now()
|
||||
today := 0
|
||||
yesterday := 0
|
||||
thisWeek := 0
|
||||
older := 0
|
||||
|
||||
for _, item := range response.Items {
|
||||
if item.GetUTCTime() > 0 {
|
||||
playTime := time.Unix(item.GetUTCTime(), 0)
|
||||
diff := now.Sub(playTime)
|
||||
|
||||
if diff < 24*time.Hour {
|
||||
today++
|
||||
} else if diff < 48*time.Hour {
|
||||
yesterday++
|
||||
} else if diff < 7*24*time.Hour {
|
||||
thisWeek++
|
||||
} else {
|
||||
older++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if today > 0 {
|
||||
fmt.Printf(" %-15s %3d items\n", "Today:", today)
|
||||
}
|
||||
if yesterday > 0 {
|
||||
fmt.Printf(" %-15s %3d items\n", "Yesterday:", yesterday)
|
||||
}
|
||||
if thisWeek > 0 {
|
||||
fmt.Printf(" %-15s %3d items\n", "This Week:", thisWeek)
|
||||
}
|
||||
if older > 0 {
|
||||
fmt.Printf(" %-15s %3d items\n", "Older:", older)
|
||||
}
|
||||
}
|
||||
|
||||
func displayResults(response *models.RecentsResponse, items []models.RecentsResponseItem, detailed bool, sourceFilter, typeFilter string) {
|
||||
// Build filter description
|
||||
var filters []string
|
||||
if sourceFilter != "" {
|
||||
filters = append(filters, fmt.Sprintf("source: %s", sourceFilter))
|
||||
}
|
||||
if typeFilter != "" {
|
||||
filters = append(filters, fmt.Sprintf("type: %s", typeFilter))
|
||||
}
|
||||
|
||||
filterDesc := ""
|
||||
if len(filters) > 0 {
|
||||
filterDesc = fmt.Sprintf(" (filtered by %s)", strings.Join(filters, ", "))
|
||||
}
|
||||
|
||||
// Display header
|
||||
fmt.Printf("\n📊 Recent Items Summary%s:\n", filterDesc)
|
||||
fmt.Printf(" Showing: %d items", len(items))
|
||||
if len(items) < response.GetItemCount() {
|
||||
fmt.Printf(" (of %d total)", response.GetItemCount())
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if len(filters) == 0 {
|
||||
// Show source breakdown for unfiltered results
|
||||
sources := []string{}
|
||||
sourceCounts := map[string]int{
|
||||
"Spotify": len(response.GetSpotifyItems()),
|
||||
"Local": len(response.GetLocalMusicItems()) + len(response.GetStoredMusicItems()),
|
||||
"TuneIn": len(response.GetTuneInItems()),
|
||||
"Pandora": len(response.GetPandoraItems()),
|
||||
}
|
||||
|
||||
for source, count := range sourceCounts {
|
||||
if count > 0 {
|
||||
sources = append(sources, fmt.Sprintf("%s: %d", source, count))
|
||||
}
|
||||
}
|
||||
|
||||
if len(sources) > 0 {
|
||||
fmt.Printf(" By Source: %s\n", strings.Join(sources, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Recent Items ===\n")
|
||||
|
||||
// Display items
|
||||
for i, item := range items {
|
||||
displayItem(i+1, &item, detailed)
|
||||
}
|
||||
|
||||
if len(items) < response.GetItemCount() {
|
||||
fmt.Printf("\n💡 Showing %d of %d total items\n", len(items), response.GetItemCount())
|
||||
fmt.Printf(" Use -limit 0 to show all items\n")
|
||||
}
|
||||
}
|
||||
|
||||
func displayItem(index int, item *models.RecentsResponseItem, detailed bool) {
|
||||
// Basic information
|
||||
displayName := item.GetDisplayName()
|
||||
source := formatSource(item.GetSource())
|
||||
contentType := item.GetContentType()
|
||||
|
||||
// Content type icon
|
||||
icon := getIcon(item)
|
||||
|
||||
fmt.Printf("%d. %s %s\n", index, icon, displayName)
|
||||
fmt.Printf(" Source: %s", source)
|
||||
|
||||
if contentType != "" {
|
||||
fmt.Printf(" | Type: %s", strings.Title(contentType))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Time information
|
||||
if item.GetUTCTime() > 0 {
|
||||
playTime := time.Unix(item.GetUTCTime(), 0)
|
||||
timeAgo := time.Since(playTime)
|
||||
fmt.Printf(" Played: %s", playTime.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf(" (%s ago)\n", formatDuration(timeAgo))
|
||||
}
|
||||
|
||||
// Additional details if requested
|
||||
if detailed {
|
||||
if item.HasID() {
|
||||
fmt.Printf(" ID: %s\n", item.GetID())
|
||||
}
|
||||
|
||||
if item.IsPresetable() {
|
||||
fmt.Printf(" ⭐ Can be saved as preset\n")
|
||||
}
|
||||
|
||||
if item.HasArtwork() {
|
||||
fmt.Printf(" 🎨 Has artwork\n")
|
||||
}
|
||||
|
||||
location := item.GetLocation()
|
||||
if location != "" {
|
||||
fmt.Printf(" 📍 Location: %s\n", truncateString(location, 60))
|
||||
}
|
||||
|
||||
sourceAccount := item.GetSourceAccount()
|
||||
if sourceAccount != "" && sourceAccount != item.GetSource() {
|
||||
fmt.Printf(" 👤 Account: %s\n", truncateString(sourceAccount, 40))
|
||||
}
|
||||
|
||||
// Content classification
|
||||
var classifications []string
|
||||
if item.IsStreamingContent() {
|
||||
classifications = append(classifications, "Streaming")
|
||||
}
|
||||
if item.IsLocalContent() {
|
||||
classifications = append(classifications, "Local")
|
||||
}
|
||||
if len(classifications) > 0 {
|
||||
fmt.Printf(" 🏷️ Type: %s\n", strings.Join(classifications, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func getIcon(item *models.RecentsResponseItem) string {
|
||||
if item.IsTrack() {
|
||||
return "🎵"
|
||||
} else if item.IsStation() {
|
||||
return "📻"
|
||||
} else if item.IsPlaylist() {
|
||||
return "📋"
|
||||
} else if item.IsAlbum() {
|
||||
return "💿"
|
||||
} else if item.IsContainer() {
|
||||
return "📁"
|
||||
}
|
||||
return "🎼"
|
||||
}
|
||||
|
||||
func formatSource(source string) string {
|
||||
switch source {
|
||||
case "SPOTIFY":
|
||||
return "Spotify"
|
||||
case "LOCAL_MUSIC":
|
||||
return "Local Music"
|
||||
case "STORED_MUSIC":
|
||||
return "Stored Music"
|
||||
case "TUNEIN":
|
||||
return "TuneIn Radio"
|
||||
case "PANDORA":
|
||||
return "Pandora"
|
||||
case "AMAZON":
|
||||
return "Amazon Music"
|
||||
case "DEEZER":
|
||||
return "Deezer"
|
||||
case "IHEART":
|
||||
return "iHeartRadio"
|
||||
case "BLUETOOTH":
|
||||
return "Bluetooth"
|
||||
case "AUX":
|
||||
return "AUX Input"
|
||||
case "AIRPLAY":
|
||||
return "AirPlay"
|
||||
default:
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return "just now"
|
||||
} else if d < time.Hour {
|
||||
minutes := int(d.Minutes())
|
||||
return fmt.Sprintf("%d minute%s", minutes, pluralize(minutes))
|
||||
} else if d < 24*time.Hour {
|
||||
hours := int(d.Hours())
|
||||
return fmt.Sprintf("%d hour%s", hours, pluralize(hours))
|
||||
} else {
|
||||
days := int(d.Hours() / 24)
|
||||
return fmt.Sprintf("%d day%s", days, pluralize(days))
|
||||
}
|
||||
}
|
||||
|
||||
func pluralize(count int) string {
|
||||
if count == 1 {
|
||||
return ""
|
||||
}
|
||||
return "s"
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLength int) string {
|
||||
if len(s) <= maxLength {
|
||||
return s
|
||||
}
|
||||
if maxLength <= 3 {
|
||||
return "..."
|
||||
}
|
||||
return s[:maxLength-3] + "..."
|
||||
}
|
||||
|
||||
func getAvailableSources(response *models.RecentsResponse) string {
|
||||
sourceMap := make(map[string]bool)
|
||||
for _, item := range response.Items {
|
||||
if source := item.GetSource(); source != "" {
|
||||
sourceMap[source] = true
|
||||
}
|
||||
}
|
||||
|
||||
var sources []string
|
||||
for source := range sourceMap {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
sort.Strings(sources)
|
||||
|
||||
if len(sources) == 0 {
|
||||
return "none"
|
||||
}
|
||||
|
||||
return strings.Join(sources, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user