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:
Tobias Gesellchen
2026-02-02 16:26:40 +01:00
parent 1ec3c6950c
commit 7ec4ee67af
23 changed files with 6410 additions and 35 deletions
+187
View File
@@ -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)
+393
View File
@@ -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
```
+148
View File
@@ -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!")
}
+279
View File
@@ -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)
+479
View File
@@ -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, ", ")
}