Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 112 additions & 15 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def search_tracks(uid: str, query: str, limit: int = 5) -> List[SpotifyTrack]:
return tracks


def get_user_playlists(uid: str, limit: int = 20) -> List[SpotifyPlaylist]:
def get_user_playlists(uid: str, limit: int = 20, onlyModifiableByUser: bool = False) -> List[SpotifyPlaylist]:
"""Get user's playlists."""
result = spotify_api_request(
uid, "GET", "/me/playlists",
Expand All @@ -208,9 +208,15 @@ def get_user_playlists(uid: str, limit: int = 20) -> List[SpotifyPlaylist]:

if "error" in result:
return []

user_profile = spotify_api_request(uid, "GET", "/me")

playlists = []
for item in result.get("items", []):
canEdit = item["owner"]["id"] == user_profile["id"] or item["collaborative"] == True
if onlyModifiableByUser and not canEdit:
continue

playlists.append(SpotifyPlaylist(
id=item["id"],
name=item["name"],
Expand All @@ -219,14 +225,15 @@ def get_user_playlists(uid: str, limit: int = 20) -> List[SpotifyPlaylist]:
tracks_total=item["tracks"]["total"],
public=item.get("public", False),
uri=item["uri"],
canEdit=canEdit,
external_url=item.get("external_urls", {}).get("spotify")
))
return playlists


def find_playlist_by_name(uid: str, name: str) -> Optional[SpotifyPlaylist]:
def find_playlist_by_name(uid: str, name: str, onlyModifiableByUser: bool = False) -> Optional[SpotifyPlaylist]:
"""Find a playlist by name (case-insensitive partial match)."""
playlists = get_user_playlists(uid, limit=50)
playlists = get_user_playlists(uid, limit=50, onlyModifiableByUser=onlyModifiableByUser)
name_lower = name.lower()

# First try exact match
Expand All @@ -242,6 +249,15 @@ def find_playlist_by_name(uid: str, name: str) -> Optional[SpotifyPlaylist]:
return None


def find_playlist_by_id(uid: str, id: str, onlyModifiableByUser: bool = False) -> Optional[SpotifyPlaylist]:
"""Find a playlist by ID."""
playlists = get_user_playlists(uid, limit=50, onlyModifiableByUser=onlyModifiableByUser)
for playlist in playlists:
if playlist.id == id:
return playlist
return None


# ============================================
# OAuth Endpoints
# ============================================
Expand Down Expand Up @@ -269,7 +285,7 @@ async def home(request: Request, uid: Optional[str] = None):
if "error" not in profile_result:
user_profile = profile_result

playlists = get_user_playlists(uid, limit=50)
playlists = get_user_playlists(uid, limit=50, onlyModifiableByUser=True)
default_playlist = get_default_playlist(uid)

return templates.TemplateResponse("setup.html", {
Expand Down Expand Up @@ -478,9 +494,9 @@ async def tool_add_to_playlist(request: Request):
)
elif playlist_name:
# Find playlist by name
target_playlist = find_playlist_by_name(uid, playlist_name)
target_playlist = find_playlist_by_name(uid, playlist_name, onlyModifiableByUser=True)
if not target_playlist:
return ChatToolResponse(error=f"Could not find playlist: {playlist_name}")
return ChatToolResponse(error=f"Could not find playlist (editable by you): {playlist_name}")
else:
# Use default playlist
default = get_default_playlist(uid)
Expand Down Expand Up @@ -594,13 +610,16 @@ async def tool_get_playlists(request: Request):
if not playlists:
return ChatToolResponse(result="You don't have any playlists yet.")

# Format results
results = []
for i, playlist in enumerate(playlists, 1):
visibility = "🌐" if playlist.public else "🔒"
results.append(f"{i}. {visibility} **{playlist.name}** ({playlist.tracks_total} tracks)")

return ChatToolResponse(result=f"📋 Your playlists:\n\n" + "\n".join(results))
return ChatToolResponse(result=str([{
"id": playlist.id,
"name": playlist.name,
"description": playlist.description,
"owner": playlist.owner,
"tracks_total": playlist.tracks_total,
"public": playlist.public,
"canEdit": playlist.canEdit,
"external_url": playlist.external_url
} for playlist in playlists]))

except Exception as e:
return ChatToolResponse(error=f"Failed to get playlists: {str(e)}")
Expand Down Expand Up @@ -762,6 +781,62 @@ async def tool_play_song(request: Request):
return ChatToolResponse(error=f"Failed to play song: {str(e)}")


@app.post("/tools/play_playlist", tags=["chat_tools"], response_model=ChatToolResponse)
async def tool_play_playlist(request: Request):
"""
Play a specific playlist on Spotify.
Chat tool for Omi - finds and plays a playlist.
"""
try:
body = await request.json()
uid = body.get("uid")
playlist_id = body.get("playlist_id")
playlist_name = body.get("playlist_name")

if not uid:
return ChatToolResponse(error="User ID is required")

if not playlist_id and not playlist_name:
return ChatToolResponse(error="Playlist ID or name is required")

# Check authentication
if not get_spotify_tokens(uid):
return ChatToolResponse(error="Please connect your Spotify account first in the app settings.")

target_playlist = None

if playlist_id:
print(f"🎵 Playlist ID provided: {playlist_id}")
target_playlist = find_playlist_by_id(uid, playlist_id)
if not target_playlist:
return ChatToolResponse(error=f"Could not find playlist with ID: {playlist_id}")
elif playlist_name:
print(f"🎵 Playlist name provided: {playlist_name}")
target_playlist = find_playlist_by_name(uid, playlist_name)
if not target_playlist:
return ChatToolResponse(error=f"Could not find playlist with name: {playlist_name}")

# Play the playlist
result = spotify_api_request(
uid, "PUT", "/me/player/play",
json_data={"context_uri": target_playlist.uri}
)

if "error" in result:
if "No active device" in str(result.get("error", "")):
return ChatToolResponse(
error="No active Spotify device found. Please open Spotify on one of your devices first."
)
return ChatToolResponse(error=f"Failed to play: {result['error']}")

return ChatToolResponse(
result=f"▶️ Now playing: **{target_playlist.name}**"
)

except Exception as e:
return ChatToolResponse(error=f"Failed to play playlist: {str(e)}")


@app.post("/tools/get_recommendations", tags=["chat_tools"], response_model=ChatToolResponse)
async def tool_get_recommendations(request: Request):
"""
Expand Down Expand Up @@ -905,7 +980,7 @@ async def get_omi_tools_manifest():
},
{
"name": "get_playlists",
"description": "Get the user's Spotify playlists. Use this when the user wants to see their playlists or check what playlists they have.",
"description": "Get the user's Spotify playlists. Use this when the user wants to see their playlists or check what playlists they have (can return playlist ids).",
"endpoint": "/tools/get_playlists",
"method": "POST",
"parameters": {
Expand Down Expand Up @@ -970,6 +1045,27 @@ async def get_omi_tools_manifest():
"auth_required": True,
"status_message": "Playing song..."
},
{
"name": "play_playlist",
"description": "Play a specific playlist on Spotify. Use this when the user wants to play a particular playlist. You can provide either the playlist ID or the playlist name.",
"endpoint": "/tools/play_playlist",
"method": "POST",
"parameters": {
"properties": {
"playlist_id": {
"type": "string",
"description": "ID of the playlist to play"
},
"playlist_name": {
"type": "string",
"description": "Name of the playlist to play"
}
},
"required": []
},
"auth_required": True,
"status_message": "Playing playlist..."
},
{
"name": "get_recommendations",
"description": "Get personalized song recommendations from Spotify. Use this when the user wants music suggestions or to discover new songs.",
Expand Down Expand Up @@ -1003,5 +1099,6 @@ async def health_check():

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)
port = int(os.environ.get("PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)

1 change: 1 addition & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SpotifyPlaylist(BaseModel):
tracks_total: int
public: bool
uri: str
canEdit: bool
external_url: Optional[str] = None


Expand Down