Add comprehensive playlist support for v1.0.0
Features: - Exact matching for all search types (artists, albums, tracks, playlists) - User playlist search (type=user_playlist) with exact and partial matching - Performance caching for user playlist data - Comprehensive documentation and test cases - HACS integration support (hacs.json) All search types now use limit=10 and prefer exact name matches to avoid Spotify's personalized recommendations, ensuring you get what you ask for.
This commit is contained in:
@@ -72,8 +72,9 @@ A lightweight integration designed specifically for voice assistants with functi
|
||||
|
||||
- ✅ **Voice-first design** - Built for natural language from day one
|
||||
- ✅ **Hardware agnostic** - Works with any Spotify Connect device
|
||||
- ✅ **Search by type** - Artists, albums, tracks, playlists
|
||||
- ✅ **Exact match preference** - Finds what you ask for, not recommendations
|
||||
- ✅ **Search by type** - Artists, albums, tracks, playlists, and your personal playlists
|
||||
- ✅ **Exact match preference** - Finds what you ask for across all content types, not recommendations
|
||||
- ✅ **Personal playlist access** - Search within your saved Spotify playlists with "play my workout playlist"
|
||||
- ✅ **Complete examples** - Extended OpenAI Conversation config included
|
||||
- ✅ **Playback control** - Pause, play, skip, volume, shuffle - all via voice
|
||||
- ✅ **Artist radio mode** - Automatically shuffles when playing artists for dynamic playlists
|
||||
@@ -121,17 +122,17 @@ Copy these functions to Extended OpenAI Conversation settings:
|
||||
```yaml
|
||||
- spec:
|
||||
name: search_spotify
|
||||
description: Search Spotify for an artist, album, or track and return the Spotify URI. Use this before playing music to get the URI.
|
||||
description: Search Spotify for an artist, album, track, or playlist and return the Spotify URI. Use this before playing music to get the URI.
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Artist, album, or track name to search for (e.g., "Coldplay", "Parachutes", "Yellow")
|
||||
description: Artist, album, track, or playlist name to search for (e.g., "Coldplay", "Parachutes", "Yellow", "Today's Top Hits")
|
||||
type:
|
||||
type: string
|
||||
enum: [artist, album, track]
|
||||
description: Type of content to search for
|
||||
enum: [artist, album, track, playlist, user_playlist]
|
||||
description: Type of content to search for. Use 'user_playlist' when user says "my playlist" or refers to their personal playlists.
|
||||
required:
|
||||
- query
|
||||
- type
|
||||
@@ -247,7 +248,8 @@ Music Playback:
|
||||
- When asked to play music, follow this two-step process: 1) Call search_spotify to get the Spotify URI, 2) Call play_music with the URI and media player entity
|
||||
- Available media players: media_player.kitchen_speaker, media_player.gaming_room_speaker, media_player.office_speaker
|
||||
- Default speaker: If no speaker is specified, use media_player.kitchen_speaker
|
||||
- Parse commands like "Play {Artist/Album/Track}" and determine the media type automatically
|
||||
- Parse commands like "Play {Artist/Album/Track/Playlist}" and determine the media type automatically (artist, album, track, playlist, or user_playlist)
|
||||
- IMPORTANT: Use type="user_playlist" when user says "my playlist", "my [playlist name]", or refers to their personal saved playlists. Use type="playlist" for public Spotify playlists like "Today's Top Hits"
|
||||
- IMPORTANT: When playing an artist, always enable shuffle after starting playback by calling control_playback with action: shuffle_on to create a dynamic playlist experience
|
||||
- For playback control (pause, skip, volume, shuffle), use the control_playback function
|
||||
```
|
||||
@@ -266,6 +268,8 @@ Music Playback:
|
||||
- "Play Coldplay on the kitchen speaker"
|
||||
- "Play the album Parachutes"
|
||||
- "Play Yellow by Coldplay"
|
||||
- "Play Today's Top Hits"
|
||||
- "Play my workout playlist"
|
||||
- "Pause the music"
|
||||
- "Skip to the next track"
|
||||
- "Set volume to 50%"
|
||||
@@ -280,6 +284,8 @@ Music Playback:
|
||||
- "I want to hear some chill music from Coldplay"
|
||||
- "Play Coldplay but shuffle it"
|
||||
- "Put on some music from that British band with Chris Martin"
|
||||
- "Play my chill vibes playlist"
|
||||
- "Put on my running music"
|
||||
|
||||
**Conversational Playback Control:**
|
||||
- "Make it louder" (instead of "Set volume to 70")
|
||||
@@ -409,17 +415,32 @@ LLM: Calls play_music(uri="spotify:artist:...", media_player="media_player.kitch
|
||||
Result: Music starts playing
|
||||
```
|
||||
|
||||
### Exact Match Artist Search
|
||||
### Exact Match Search
|
||||
|
||||
When searching for artists, this integration uses smart matching to avoid Spotify's personalization issues:
|
||||
This integration uses smart matching across all content types to avoid Spotify's personalization issues:
|
||||
|
||||
1. Queries Spotify for top 10 artist results
|
||||
1. Queries Spotify for top 10 results (instead of just 1)
|
||||
2. Checks each result for exact name match (case-insensitive)
|
||||
3. Returns exact match if found, otherwise returns first result
|
||||
4. Logs match type (exact/first) for debugging
|
||||
4. Logs match type (exact/first/partial) for debugging
|
||||
|
||||
**Applies to:** Artists, albums, tracks, and playlists
|
||||
|
||||
**Why this matters:** Standard Spotify search prioritizes personalized recommendations. If you search "Coldplay," you might get Taylor Swift if you listen to her frequently. Our exact matching ensures you get Coldplay when you ask for Coldplay.
|
||||
|
||||
### User Playlist Search
|
||||
|
||||
When you say "my playlist" or "my [playlist name]", the integration searches only within your saved Spotify playlists:
|
||||
|
||||
1. Retrieves your personal Spotify library playlists
|
||||
2. First looks for exact name match (case-insensitive)
|
||||
3. Falls back to partial match if no exact match found
|
||||
4. Returns helpful error if no matching playlist exists
|
||||
|
||||
**Examples:**
|
||||
- "Play my workout playlist" → Searches only your saved playlists
|
||||
- "Play Today's Top Hits" → Searches all public Spotify playlists
|
||||
|
||||
### No Additional Authentication
|
||||
|
||||
The integration leverages Home Assistant's official Spotify integration:
|
||||
@@ -432,19 +453,42 @@ The integration leverages Home Assistant's official Spotify integration:
|
||||
|
||||
The integration includes smart caching for optimal performance:
|
||||
|
||||
**First search:** ~1.6-5.6ms (finds and caches Spotify client)
|
||||
**Subsequent searches:** ~0.1ms (uses cached client)
|
||||
**Performance gain:** 15-50x faster on repeated searches
|
||||
**Spotify Client Cache:**
|
||||
- **First search:** ~1.6-5.6ms (finds and caches Spotify client)
|
||||
- **Subsequent searches:** ~0.1ms (uses cached client)
|
||||
- **Performance gain:** 15-50x faster on repeated searches
|
||||
|
||||
**User Playlist Cache:**
|
||||
- **First user_playlist search:** Fetches all user playlists from Spotify API
|
||||
- **Subsequent user_playlist searches:** Uses cached playlist data
|
||||
- **Performance gain:** Significantly faster for users with large playlist libraries
|
||||
|
||||
**Cache Features:**
|
||||
- Automatically validates cached client on every call
|
||||
- Self-invalidates when Spotify integration is reloaded or removed
|
||||
- Caches user playlists to avoid repeated API calls
|
||||
- Manual cache clearing available via `spotify_search.clear_cache` service
|
||||
|
||||
The cache is module-level and process-scoped, so it clears automatically on Home Assistant restart.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Search Types
|
||||
|
||||
The integration supports five search types:
|
||||
|
||||
| Type | Description | Example Query | Use Case |
|
||||
|------|-------------|---------------|----------|
|
||||
| `artist` | Search all Spotify artists | "Coldplay" | Play an artist's music |
|
||||
| `album` | Search all Spotify albums | "Parachutes" | Play a specific album |
|
||||
| `track` | Search all Spotify tracks | "Yellow" | Play a specific song |
|
||||
| `playlist` | Search all public Spotify playlists | "Today's Top Hits" | Play curated or public playlists |
|
||||
| `user_playlist` | Search only your saved playlists | "workout" | Play your personal playlists |
|
||||
|
||||
**Key difference:**
|
||||
- `playlist` - Searches all of Spotify's public playlists
|
||||
- `user_playlist` - Searches only playlists you've saved/created
|
||||
|
||||
### Developer Tools Testing
|
||||
|
||||
Test the search service directly:
|
||||
@@ -465,6 +509,24 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
Test user playlist search:
|
||||
|
||||
```yaml
|
||||
service: spotify_search.search
|
||||
data:
|
||||
query: "workout"
|
||||
type: "user_playlist"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"uri": "spotify:playlist:37i9dQZF1DX76Wlfdnj7AP",
|
||||
"name": "Workout Beats",
|
||||
"type": "playlist"
|
||||
}
|
||||
```
|
||||
|
||||
### Use in Automations
|
||||
|
||||
```yaml
|
||||
@@ -503,12 +565,21 @@ This approach leverages Spotify's search algorithm to naturally weight both term
|
||||
|
||||
### Clear Cache Service
|
||||
|
||||
If you experience issues after removing or re-adding the Spotify integration, you can manually clear the cached client:
|
||||
If you experience issues or want to refresh cached data (e.g., after adding new playlists to your Spotify library), you can manually clear the cache:
|
||||
|
||||
```yaml
|
||||
service: spotify_search.clear_cache
|
||||
```
|
||||
|
||||
This clears:
|
||||
- Cached Spotify client reference
|
||||
- Cached user playlist data
|
||||
|
||||
**When to use:**
|
||||
- After adding/removing playlists in Spotify (to refresh user_playlist searches)
|
||||
- After removing or re-adding the Spotify integration
|
||||
- If experiencing unexpected search results
|
||||
|
||||
The cache automatically invalidates when the Spotify integration is reloaded, so manual clearing is rarely needed.
|
||||
|
||||
### Debug Logging
|
||||
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
# Voice Command Test Cases
|
||||
|
||||
Test suite for validating Spotify Voice Assistant behavior with natural language queries.
|
||||
|
||||
## How to Use This
|
||||
|
||||
1. Say each command to your voice assistant
|
||||
2. Note the actual response and whether it worked correctly
|
||||
3. Use results to identify areas for improvement
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Basic Artist Queries
|
||||
|
||||
**Test 1.1: Simple artist request**
|
||||
- **Command:** "Play Coldplay"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 1.2: Artist with speaker specified**
|
||||
- **Command:** "Play Coldplay on the kitchen speaker"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on kitchen speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 1.3: Natural language artist request**
|
||||
- **Command:** "I'm in the mood for some Coldplay"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 1.4: Artist with contextual description**
|
||||
- **Command:** "Play that British band with Chris Martin"
|
||||
- **Expected:** LLM interprets as Coldplay, searches and plays
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 2. Album Queries
|
||||
|
||||
**Test 2.1: Specific album**
|
||||
- **Command:** "Play the album Parachutes"
|
||||
- **Expected:** Searches for album "Parachutes", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 2.2: Album with artist (combined query)**
|
||||
- **Command:** "Play Parachutes by Coldplay"
|
||||
- **Expected:** Searches for album "Parachutes Coldplay", plays correct album
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 2.3: Natural language album request**
|
||||
- **Command:** "Can you play the Parachutes album?"
|
||||
- **Expected:** Searches for album "Parachutes", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 2.4: Album with speaker**
|
||||
- **Command:** "Play the album Parachutes on the gaming room speaker"
|
||||
- **Expected:** Searches for album "Parachutes", plays on gaming room speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 2.5: Album exact match**
|
||||
- **Command:** "Play Parachutes"
|
||||
- **Expected:** Searches albums with limit=10, returns exact match for "Parachutes" (not "Parachutes Deluxe Edition")
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 3. Track Queries
|
||||
|
||||
**Test 3.1: Specific track**
|
||||
- **Command:** "Play Yellow"
|
||||
- **Expected:** Searches for track "Yellow", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 3.2: Track with artist (combined query)**
|
||||
- **Command:** "Play Yellow by Coldplay"
|
||||
- **Expected:** Searches for track "Yellow Coldplay", plays correct track
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 3.3: Track with speaker**
|
||||
- **Command:** "Play the song Yellow on the office speaker"
|
||||
- **Expected:** Searches for track "Yellow", plays on office speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 3.4: Natural language track request**
|
||||
- **Command:** "Put on that song Yellow"
|
||||
- **Expected:** Searches for track "Yellow", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 3.5: Track exact match**
|
||||
- **Command:** "Play Yellow"
|
||||
- **Expected:** Searches tracks with limit=10, returns exact match for "Yellow" (prioritizes exact name over similar songs)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 4. Ambiguous Track Names (Edge Cases)
|
||||
|
||||
**Test 4.1: Common track name (no artist)**
|
||||
- **Command:** "Play Hurt"
|
||||
- **Expected:** Searches for track "Hurt", returns most popular (likely Johnny Cash or Nine Inch Nails)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 4.2: Common track name with artist**
|
||||
- **Command:** "Play Hurt by Nine Inch Nails"
|
||||
- **Expected:** Searches for track "Hurt Nine Inch Nails", returns correct version
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 4.3: Common track name with different artist**
|
||||
- **Command:** "Play Hurt by Johnny Cash"
|
||||
- **Expected:** Searches for track "Hurt Johnny Cash", returns Cash cover version
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 5. Exact Match Testing (All Content Types)
|
||||
|
||||
**Test 5.1: Exact artist name**
|
||||
- **Command:** "Play Coldplay"
|
||||
- **Expected:** Returns exact match for artist "Coldplay" (not similar/recommended artists)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 5.2: Similar artist name**
|
||||
- **Command:** "Play Cold War Kids"
|
||||
- **Expected:** Returns exact match for "Cold War Kids" (not "Coldplay" despite similarity)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 5.3: Artist with common name**
|
||||
- **Command:** "Play The Band"
|
||||
- **Expected:** Returns exact match for artist "The Band" (not generic band recommendations)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 5.4: Album exact match verification**
|
||||
- **Command:** "Play A Rush of Blood to the Head"
|
||||
- **Expected:** Returns exact match for album name, not "Deluxe" or "Remastered" versions
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 5.5: Track exact match verification**
|
||||
- **Command:** "Play Fix You"
|
||||
- **Expected:** Returns exact match for track "Fix You" from top 10 results
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 6. Playlist Queries
|
||||
|
||||
**Test 6.1: Public playlist - exact match**
|
||||
- **Command:** "Play Today's Top Hits"
|
||||
- **Expected:** Searches public playlists, returns exact match for "Today's Top Hits"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 6.2: Public playlist - common name**
|
||||
- **Command:** "Play Chill Vibes"
|
||||
- **Expected:** Searches public playlists with limit=10, prefers exact "Chill Vibes" match over "Chill Vibes Mix"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 6.3: User playlist - exact match**
|
||||
- **Command:** "Play my workout playlist"
|
||||
- **Expected:** Searches only user's saved playlists, finds exact match for "workout"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 6.4: User playlist - partial match**
|
||||
- **Command:** "Play my running music"
|
||||
- **Expected:** If no exact "running music" match, finds partial match like "Running Music 2024"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 6.5: User playlist - not found**
|
||||
- **Command:** "Play my xyz123 playlist"
|
||||
- **Expected:** Returns error "No playlist matching 'xyz123' found in your library"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 6.6: Playlist vs user playlist distinction**
|
||||
- **Command 1:** "Play RapCaviar"
|
||||
- **Expected 1:** Uses type=playlist, searches all public playlists
|
||||
- **Command 2:** "Play my RapCaviar"
|
||||
- **Expected 2:** Uses type=user_playlist, searches only user's saved playlists (assuming user has saved RapCaviar)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 7. Playback Control
|
||||
|
||||
**Test 7.1: Pause**
|
||||
- **Command:** "Pause the music"
|
||||
- **Expected:** Pauses current playback on active speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.2: Resume**
|
||||
- **Command:** "Resume"
|
||||
- **Expected:** Resumes playback on active speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.3: Next track**
|
||||
- **Command:** "Skip to the next track"
|
||||
- **Expected:** Advances to next track
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.4: Previous track**
|
||||
- **Command:** "Go back to the previous song"
|
||||
- **Expected:** Returns to previous track
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.5: Volume up (natural language)**
|
||||
- **Command:** "Make it louder"
|
||||
- **Expected:** Increases volume (LLM interprets as volume_set with higher level)
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.6: Volume specific**
|
||||
- **Command:** "Set volume to 50 percent"
|
||||
- **Expected:** Sets volume to 50%
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 7.7: Stop**
|
||||
- **Command:** "Stop the music"
|
||||
- **Expected:** Stops playback
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 8. Multi-Speaker Scenarios
|
||||
|
||||
**Test 8.1: Different speaker each time**
|
||||
- **Commands:**
|
||||
- "Play Coldplay on the kitchen speaker"
|
||||
- "Play Radiohead on the gaming room speaker"
|
||||
- "Play Muse on the office speaker"
|
||||
- **Expected:** Each plays on the specified speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 8.2: No speaker specified (uses default)**
|
||||
- **Command:** "Play Coldplay"
|
||||
- **Expected:** Plays on default speaker from system prompt
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 9. Error Handling
|
||||
|
||||
**Test 9.1: Non-existent artist**
|
||||
- **Command:** "Play XYZ123NotARealBand"
|
||||
- **Expected:** Returns no results or best guess, handles gracefully
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 9.2: Typo in artist name**
|
||||
- **Command:** "Play Cold Play" (with space)
|
||||
- **Expected:** Spotify's fuzzy search finds "Coldplay"
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 9.3: Invalid speaker**
|
||||
- **Command:** "Play Coldplay on the bedroom speaker" (if bedroom speaker doesn't exist)
|
||||
- **Expected:** Error or fallback behavior
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
### 10. Natural Language Variations
|
||||
|
||||
**Test 10.1: Informal request**
|
||||
- **Command:** "Put on some Coldplay"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 10.2: Question format**
|
||||
- **Command:** "Can you play Coldplay?"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 10.3: Context-heavy request**
|
||||
- **Command:** "I want to listen to Coldplay right now"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
**Test 10.4: Mood-based request**
|
||||
- **Command:** "Play something chill from Coldplay"
|
||||
- **Expected:** Searches for artist "Coldplay", plays on default speaker
|
||||
- **Result:**
|
||||
- **Pass/Fail:**
|
||||
|
||||
---
|
||||
|
||||
## Results Summary
|
||||
|
||||
**Total Tests:** 40
|
||||
**Passed:** _____
|
||||
**Failed:** _____
|
||||
**Pass Rate:** _____%
|
||||
|
||||
### Common Failure Patterns
|
||||
(Document patterns that emerge from testing)
|
||||
|
||||
### Recommendations for Improvement
|
||||
(Based on test results, list specific enhancements needed)
|
||||
@@ -1,12 +1,10 @@
|
||||
# TODO
|
||||
|
||||
## v1.0.0 Release
|
||||
- [ ] Create GitHub release with notes
|
||||
- [ ] Test installation via HACS custom repository
|
||||
Development roadmap and enhancement proposals for Spotify Voice Assistant.
|
||||
|
||||
## Future Enhancements (v1.1.0+)
|
||||
## Future Enhancements
|
||||
|
||||
### Artist Filter Parameter
|
||||
### 1. Artist Filter Parameter
|
||||
Add optional artist filter parameter to improve multi-part query accuracy.
|
||||
|
||||
**Current approach (v1.0.0):**
|
||||
@@ -14,13 +12,13 @@ Add optional artist filter parameter to improve multi-part query accuracy.
|
||||
- Relies on Spotify's search ranking algorithm
|
||||
- Works well via LLM prompt enhancement
|
||||
|
||||
**Proposed enhancement (v1.1.0):**
|
||||
**Proposed enhancement:**
|
||||
- Add `artist` parameter to `search_spotify` service
|
||||
- Apply artist filtering to search results
|
||||
- Example: `search_spotify(query="Yellow", type="track", artist="Coldplay")`
|
||||
|
||||
**Benefits:**
|
||||
- More precise matching when multiple artists have songs with same name
|
||||
- More precise matching when multiple artists have songs with same name (e.g., "Hurt" by Nine Inch Nails vs Johnny Cash)
|
||||
- Explicit artist filtering vs relying on search ranking
|
||||
- Better handling of edge cases
|
||||
|
||||
@@ -35,3 +33,97 @@ Add optional artist filter parameter to improve multi-part query accuracy.
|
||||
- Requires more specific parameter extraction from LLM
|
||||
- May reduce flexibility of natural language queries
|
||||
- Current combined query approach already works well for most cases
|
||||
|
||||
---
|
||||
|
||||
### 2. ~~Extend Exact Name Matching to Albums and Tracks~~ ✅ COMPLETED (v1.0.0)
|
||||
~~Apply the same "exact match" logic currently used for artists to albums and tracks.~~
|
||||
|
||||
**Status:** Implemented in v1.0.0
|
||||
- All search types (artist, album, track, playlist) now use exact name matching
|
||||
- Searches use limit=10 and check for exact matches before falling back to first result
|
||||
- Consistent behavior across all content types
|
||||
- Logs indicate match type (exact/first/partial) for debugging
|
||||
|
||||
**Also implemented:**
|
||||
- Playlist exact matching with same logic
|
||||
- User playlist search with exact and partial matching
|
||||
- Caching for user playlists to improve performance
|
||||
|
||||
---
|
||||
|
||||
### 3. Configuration Options
|
||||
Allow users to configure integration behavior via Home Assistant UI or YAML.
|
||||
|
||||
**Proposed configurable options:**
|
||||
- Number of results to check for exact match (currently hardcoded to 10)
|
||||
- Whether to require exact match or allow fuzzy matching
|
||||
- Default search type (artist/album/track) when not specified
|
||||
- Cache duration/behavior
|
||||
- Logging verbosity
|
||||
|
||||
**Current behavior:**
|
||||
- All behavior is hardcoded in the integration
|
||||
|
||||
**Benefits:**
|
||||
- Users can tune behavior for their specific needs without modifying code
|
||||
- Easier troubleshooting with configurable logging
|
||||
- Flexibility for different use cases
|
||||
|
||||
**Implementation notes:**
|
||||
- Add config flow for UI-based configuration
|
||||
- Support YAML configuration in `configuration.yaml`
|
||||
- Ensure sensible defaults
|
||||
- Document all options in README
|
||||
|
||||
---
|
||||
|
||||
### 4. Integration with HA Assist
|
||||
Make the integration work with Home Assistant's built-in voice pipeline (Assist) without requiring Extended OpenAI Conversation.
|
||||
|
||||
**Current behavior:**
|
||||
- Requires Extended OpenAI Conversation (or similar) to provide function calling interface
|
||||
|
||||
**Proposed enhancement:**
|
||||
- Register as a native Home Assistant intent/sentence pattern
|
||||
- Allow Assist to use it directly via standard voice pipeline
|
||||
|
||||
**Benefits:**
|
||||
- Users who want to use HA's default voice pipeline (Whisper → built-in LLM → actions) could use this integration
|
||||
- Lower barrier to entry for non-technical users
|
||||
|
||||
**Trade-offs:**
|
||||
- Would likely still require intent patterns like "Play [artist] on [device]"
|
||||
- Less flexible than LLM-based approach
|
||||
- May lose natural language advantage
|
||||
|
||||
**Implementation notes:**
|
||||
- Register intent handlers via Home Assistant's conversation integration
|
||||
- Define sentence patterns for common use cases
|
||||
- Consider maintaining both approaches (function calling + intent patterns)
|
||||
|
||||
---
|
||||
|
||||
### 5. Saved/Favorite Content Quick Access
|
||||
Add ability to search/play from user's Spotify saved content (liked songs, saved albums, followed artists).
|
||||
|
||||
**Status:** Partially implemented in v1.0.0
|
||||
- ✅ User playlists: `type="user_playlist"` searches only saved playlists
|
||||
- ✅ Caching implemented for user playlist data
|
||||
- ⏳ Future: Saved tracks, albums, and followed artists
|
||||
|
||||
**Implemented in v1.0.0:**
|
||||
- Voice commands like "Play my workout playlist" now work
|
||||
- Searches only user's saved playlists (not all of Spotify)
|
||||
- Exact and partial matching for user playlists
|
||||
- Performance-optimized with caching
|
||||
|
||||
**Still to implement:**
|
||||
- `type="saved_tracks"` for liked songs
|
||||
- `type="saved_albums"` for saved albums
|
||||
- `type="followed_artists"` for followed artists
|
||||
|
||||
**Implementation notes:**
|
||||
- Use Spotify API endpoints: `get_saved_tracks()`, `get_saved_albums()`
|
||||
- Handle pagination for large libraries
|
||||
- Apply similar caching strategy as user playlists
|
||||
|
||||
@@ -7,12 +7,13 @@ from homeassistant.helpers.typing import ConfigType
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "spotify_search"
|
||||
VALID_SEARCH_TYPES = {"artist", "album", "track", "playlist"}
|
||||
VALID_SEARCH_TYPES = {"artist", "album", "track", "playlist", "user_playlist"}
|
||||
|
||||
# Cache Spotify client to avoid repeated lookups
|
||||
_spotify_cache = {
|
||||
"client": None,
|
||||
"entity_id": None,
|
||||
"user_playlists": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -140,21 +141,122 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
else:
|
||||
_LOGGER.warning("No artists found for query: %s", query)
|
||||
return {"error": f"No artist found for: {query}"}
|
||||
else:
|
||||
# For albums, tracks, playlists - use direct search
|
||||
results = await client.search(query, [search_type], limit=1)
|
||||
items_list = getattr(results, f"{search_type}s", None)
|
||||
elif search_type == "playlist":
|
||||
# For playlists, search with higher limit and check for exact matches
|
||||
_LOGGER.debug("Searching for playlist: %s", query)
|
||||
results = await client.search(query, ["playlist"], limit=10)
|
||||
items_list = results.playlists
|
||||
if items_list and len(items_list) > 0:
|
||||
item = items_list[0]
|
||||
# Check if any playlist name matches exactly (case-insensitive)
|
||||
exact_match = None
|
||||
query_lower = query.lower()
|
||||
for playlist in items_list:
|
||||
# Defensive attribute check
|
||||
if hasattr(playlist, "name") and playlist.name.lower() == query_lower:
|
||||
exact_match = playlist
|
||||
break
|
||||
|
||||
# Use exact match if found, otherwise use first result
|
||||
selected_playlist = exact_match if exact_match else items_list[0]
|
||||
|
||||
# Defensive attribute checks
|
||||
if not hasattr(item, "uri") or not hasattr(item, "name"):
|
||||
if not hasattr(selected_playlist, "uri") or not hasattr(selected_playlist, "name"):
|
||||
_LOGGER.error("Playlist result missing required attributes")
|
||||
return {"error": "Invalid playlist data from Spotify"}
|
||||
|
||||
uri = selected_playlist.uri
|
||||
name = selected_playlist.name
|
||||
match_type = "exact match" if exact_match else "first result"
|
||||
_LOGGER.info("Found Spotify playlist: %s (%s) - %s", name, uri, match_type)
|
||||
return {"uri": uri, "name": name, "type": "playlist"}
|
||||
else:
|
||||
_LOGGER.warning("No playlists found for query: %s", query)
|
||||
return {"error": f"No playlist found for: {query}"}
|
||||
elif search_type == "user_playlist":
|
||||
# Search within user's saved playlists
|
||||
_LOGGER.debug("Searching user's playlists for: %s", query)
|
||||
try:
|
||||
# Get user's playlists (with caching)
|
||||
if _spotify_cache["user_playlists"] is None:
|
||||
_LOGGER.debug("Cache miss, fetching user playlists")
|
||||
user_playlists = await client.get_playlists_for_current_user()
|
||||
_spotify_cache["user_playlists"] = user_playlists
|
||||
else:
|
||||
_LOGGER.debug("Using cached user playlists")
|
||||
user_playlists = _spotify_cache["user_playlists"]
|
||||
|
||||
if not user_playlists or not hasattr(user_playlists, "items"):
|
||||
_LOGGER.warning("No user playlists found or invalid response")
|
||||
return {"error": "Could not retrieve user playlists"}
|
||||
|
||||
items_list = user_playlists.items
|
||||
if not items_list or len(items_list) == 0:
|
||||
_LOGGER.warning("User has no saved playlists")
|
||||
return {"error": "No saved playlists found"}
|
||||
|
||||
# Search for exact match in user's playlists
|
||||
exact_match = None
|
||||
query_lower = query.lower()
|
||||
for playlist in items_list:
|
||||
if hasattr(playlist, "name") and playlist.name.lower() == query_lower:
|
||||
exact_match = playlist
|
||||
break
|
||||
|
||||
# If no exact match, search for partial match
|
||||
partial_match = None
|
||||
if not exact_match:
|
||||
for playlist in items_list:
|
||||
if hasattr(playlist, "name") and query_lower in playlist.name.lower():
|
||||
partial_match = playlist
|
||||
break
|
||||
|
||||
selected_playlist = exact_match or partial_match
|
||||
|
||||
if not selected_playlist:
|
||||
_LOGGER.warning("No matching playlist found in user's library for: %s", query)
|
||||
return {"error": f"No playlist matching '{query}' found in your library"}
|
||||
|
||||
# Defensive attribute checks
|
||||
if not hasattr(selected_playlist, "uri") or not hasattr(selected_playlist, "name"):
|
||||
_LOGGER.error("User playlist result missing required attributes")
|
||||
return {"error": "Invalid playlist data"}
|
||||
|
||||
uri = selected_playlist.uri
|
||||
name = selected_playlist.name
|
||||
match_type = "exact match" if exact_match else "partial match"
|
||||
_LOGGER.info("Found user playlist: %s (%s) - %s", name, uri, match_type)
|
||||
return {"uri": uri, "name": name, "type": "playlist"}
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error retrieving user playlists: %s", err)
|
||||
return {"error": "Failed to retrieve user playlists"}
|
||||
else:
|
||||
# For albums, tracks - search with higher limit and check for exact matches
|
||||
_LOGGER.debug("Searching for %s: %s", search_type, query)
|
||||
results = await client.search(query, [search_type], limit=10)
|
||||
items_list = getattr(results, f"{search_type}s", None)
|
||||
if items_list and len(items_list) > 0:
|
||||
# Check if any item name matches exactly (case-insensitive)
|
||||
exact_match = None
|
||||
query_lower = query.lower()
|
||||
for item in items_list:
|
||||
# Defensive attribute check
|
||||
if hasattr(item, "name") and item.name.lower() == query_lower:
|
||||
exact_match = item
|
||||
break
|
||||
|
||||
# Use exact match if found, otherwise use first result
|
||||
selected_item = exact_match if exact_match else items_list[0]
|
||||
|
||||
# Defensive attribute checks
|
||||
if not hasattr(selected_item, "uri") or not hasattr(selected_item, "name"):
|
||||
_LOGGER.error("%s result missing required attributes", search_type)
|
||||
return {"error": f"Invalid {search_type} data from Spotify"}
|
||||
|
||||
uri = item.uri
|
||||
name = item.name
|
||||
_LOGGER.info("Found Spotify %s: %s (%s)", search_type, name, uri)
|
||||
uri = selected_item.uri
|
||||
name = selected_item.name
|
||||
match_type = "exact match" if exact_match else "first result"
|
||||
_LOGGER.info("Found Spotify %s: %s (%s) - %s", search_type, name, uri, match_type)
|
||||
return {"uri": uri, "name": name, "type": search_type}
|
||||
else:
|
||||
_LOGGER.warning("No results found for query: %s", query)
|
||||
@@ -168,11 +270,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return {"error": "Search failed"}
|
||||
|
||||
async def clear_cache(call: ServiceCall):
|
||||
"""Clear Spotify client cache."""
|
||||
if _spotify_cache["client"] is not None:
|
||||
_LOGGER.info("Manually clearing Spotify client cache")
|
||||
"""Clear Spotify client and user playlists cache."""
|
||||
if _spotify_cache["client"] is not None or _spotify_cache["user_playlists"] is not None:
|
||||
_LOGGER.info("Manually clearing Spotify cache")
|
||||
_spotify_cache["client"] = None
|
||||
_spotify_cache["entity_id"] = None
|
||||
_spotify_cache["user_playlists"] = None
|
||||
return {"success": True, "message": "Cache cleared"}
|
||||
else:
|
||||
return {"success": False, "message": "Cache was already empty"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
search:
|
||||
name: Search Spotify
|
||||
description: Search Spotify for artists, albums, tracks, or playlists and return the Spotify URI. Uses exact match when available for artist searches. Leverages your existing Home Assistant Spotify integration - no additional authentication needed.
|
||||
description: Search Spotify for artists, albums, tracks, or playlists and return the Spotify URI. Uses exact match when available. Leverages your existing Home Assistant Spotify integration - no additional authentication needed.
|
||||
fields:
|
||||
query:
|
||||
name: Query
|
||||
@@ -11,7 +11,7 @@ search:
|
||||
text:
|
||||
type:
|
||||
name: Type
|
||||
description: Type of content to search for. Artist search uses exact name matching to avoid personalized recommendations.
|
||||
description: Type of content to search for. All search types use exact name matching when possible. Use 'user_playlist' to search only within your saved playlists.
|
||||
required: false
|
||||
default: "artist"
|
||||
example: "artist"
|
||||
@@ -22,7 +22,8 @@ search:
|
||||
- "album"
|
||||
- "track"
|
||||
- "playlist"
|
||||
- "user_playlist"
|
||||
|
||||
clear_cache:
|
||||
name: Clear Cache
|
||||
description: Clear the cached Spotify client. The integration caches the Spotify client reference for performance (15-50x faster searches). Use this service if you experience issues after removing or re-adding the Spotify integration. The cache automatically invalidates when the Spotify integration is reloaded.
|
||||
description: Clear the cached Spotify client and user playlist data. The integration caches for performance (15-50x faster searches). Use this service after adding/removing playlists in Spotify or if you experience issues after removing or re-adding the Spotify integration. The cache automatically invalidates when the Spotify integration is reloaded.
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
|
||||
- spec:
|
||||
name: search_spotify
|
||||
description: Search Spotify for an artist, album, or track and return the Spotify URI. Use this before playing music to get the URI.
|
||||
description: Search Spotify for an artist, album, track, or playlist and return the Spotify URI. Use this before playing music to get the URI.
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Artist, album, or track name to search for (e.g., "Coldplay", "Parachutes", "Yellow")
|
||||
description: Artist, album, track, or playlist name to search for (e.g., "Coldplay", "Parachutes", "Yellow", "Today's Top Hits")
|
||||
type:
|
||||
type: string
|
||||
enum: [artist, album, track]
|
||||
description: Type of content to search for
|
||||
enum: [artist, album, track, playlist, user_playlist]
|
||||
description: Type of content to search for. Use 'user_playlist' when user says "my playlist" or refers to their personal playlists.
|
||||
required:
|
||||
- query
|
||||
- type
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
Music Playback:
|
||||
- When asked to play music, follow this two-step process: 1) Call search_spotify to get the Spotify URI, 2) Call play_music with the URI and media player entity
|
||||
- Available media players: media_player.kitchen_speaker, media_player.living_room_speaker (customize with your actual entity IDs)
|
||||
- Parse commands like "Play {Artist/Album/Track}" and determine the media type automatically (artist, album, or track)
|
||||
- Parse commands like "Play {Artist/Album/Track/Playlist}" and determine the media type automatically (artist, album, track, playlist, or user_playlist)
|
||||
- IMPORTANT: Use type="user_playlist" when user says "my playlist", "my [playlist name]", or refers to their personal saved playlists. Use type="playlist" for public Spotify playlists like "Today's Top Hits"
|
||||
- IMPORTANT: When playing an artist, always enable shuffle after starting playback by calling control_playback with action: shuffle_on to create a dynamic playlist experience
|
||||
- For playback control (pause, skip, volume, shuffle), use the control_playback function
|
||||
- Always use natural language interpretation - users will say things like "play Coldplay" not "search for artist Coldplay"
|
||||
- Always use natural language interpretation - users will say things like "play Coldplay" or "play my workout playlist" not "search for artist Coldplay"
|
||||
|
||||
Reference in New Issue
Block a user