From 5be093af6d54c7598af675b990c1096bf84f8c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=A1=D0=B0=D0=BF=D0=BA?= =?UTF-8?q?=D0=BE?= Date: Mon, 23 Mar 2026 15:00:11 +0200 Subject: [PATCH 1/4] add podcast player --- community/podcast-player/README.md | 103 ++++++++++ community/podcast-player/__init__.py | 0 community/podcast-player/main.py | 277 +++++++++++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 community/podcast-player/README.md create mode 100644 community/podcast-player/__init__.py create mode 100644 community/podcast-player/main.py diff --git a/community/podcast-player/README.md b/community/podcast-player/README.md new file mode 100644 index 00000000..e4d3d38b --- /dev/null +++ b/community/podcast-player/README.md @@ -0,0 +1,103 @@ +# Podcast Player + +## What It Does + +A fully voice-controlled podcast player powered by the Listen Notes API. +Search for podcast episodes, browse podcasts, get the latest episodes, or say “surprise me” to instantly stream a random episode. + +The ability streams podcast audio directly and supports natural conversational navigation. + +## Suggested Trigger Words + +- podcast + +- listen to podcast + +- search podcast + +- play episode + +- surprise me + +## Setup + +- Get a free API key from Listen Notes: https://www.listennotes.com/api/dashboard/#apps + +- Open main.py and replace: + +- API_KEY = "YOUR_LISTEN_NOTES_API_KEY" + +- with your actual API key. + +## How It Works + +User triggers the ability (e.g., “podcast”) + +Assistant asks what the user would like to listen to + +User can: + +Search for an episode + +Search for a podcast and browse its latest episodes + +Ask for a random episode + +The assistant streams the episode audio live + +User can: + +Ask what’s currently playing + +Choose another episode + +Stop playback + +## Features + +- 🎙️Episode Search — Find podcast episodes by topic or keywords + +- 📻 Podcast Search — Browse podcasts and select from recent episodes + +- 🎲 Surprise Mode — Instantly play a random episode + +- 🔊 Live Audio Streaming — Streams audio directly within the session + +- ❓ What’s Playing — Ask what episode is currently playing + +- ⛔ Voice Exit Control — Stop playback anytime + +Example Conversation + +**User:**: podcast +**AI:**: What would you like to listen to? You can search for an episode, a podcast, or say surprise me. + +**User:**: search artificial intelligence +**AI:**: Here are a few options: + +The Future of AI from Tech Today, 42 minutes. + +AI in Healthcare from Innovation Weekly, 35 minutes. + +Machine Learning Basics from Data Talks, 28 minutes. +Choose 1, 2, or 3. + +**User:**: first +**AI:**: Playing The Future of AI from Tech Today. +(audio streams) + +**User:**: what’s playing +**AI:**: You're listening to The Future of AI from Tech Today. + +**User:**: stop +**AI:**: Stopping playback. + +## Notes + +- Requires an active internet connection + +- Uses the Listen Notes public podcast API + +- Some episodes may not contain playable audio URLs + +- treaming performance depends on the source audio host diff --git a/community/podcast-player/__init__.py b/community/podcast-player/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/podcast-player/main.py b/community/podcast-player/main.py new file mode 100644 index 00000000..7f63fbba --- /dev/null +++ b/community/podcast-player/main.py @@ -0,0 +1,277 @@ +import requests +from src.agent.capability import MatchingCapability +from src.main import AgentWorker +from src.agent.capability_worker import CapabilityWorker + +API_KEY = "YOUR_LISTEN_NOTES_API_KEY" # YOUR KEY from https://www.listennotes.com/api/dashboard/#apps +BASE_URL = "https://listen-api.listennotes.com/api/v2" + +EXIT_WORDS = {"stop", "pause", "exit", "quit", "cancel"} +SURPRISE_WORDS = {"surprise", "random", "anything"} +SEARCH_WORDS = {"find", "search", "podcast", "listen"} +ELSE_WORDS = {"something else", "another one"} +WHATS_PLAYING_WORDS = {"what's playing", "what is playing", "current"} + + +class PodcastPlayerCapability(MatchingCapability): + worker: AgentWorker = None + capability_worker: CapabilityWorker = None + + # {{register capability}} + + def call(self, worker: AgentWorker): + self.worker = worker + self.capability_worker = CapabilityWorker(self.worker) + self.worker.session_tasks.create(self.run()) + + # ------------------------------------------------------------------------- + # Helpers + # ------------------------------------------------------------------------- + + def _headers(self): + return {"X-ListenAPI-Key": API_KEY} + + def _wants(self, text: str, words: set[str]) -> bool: + t = text.lower() + return any(w in t for w in words) + + # ------------------------------------------------------------------------- + # API Calls + # ------------------------------------------------------------------------- + + def search_episodes(self, query: str): + url = f"{BASE_URL}/search" + params = {"q": query, "type": "episode", "sort_by_date": 0, "page_size": 5} + response = requests.get(url, headers=self._headers(), params=params, timeout=10) + response.raise_for_status() + return response.json().get("results", []) + + def search_podcasts(self, query: str): + url = f"{BASE_URL}/search" + params = {"q": query, "type": "podcast", "page_size": 5} + response = requests.get(url, headers=self._headers(), params=params, timeout=10) + response.raise_for_status() + return response.json().get("results", []) + + def random_episode(self): + url = f"{BASE_URL}/just_listen" + response = requests.get(url, headers=self._headers(), timeout=10) + response.raise_for_status() + return response.json() + + def get_podcast_episodes(self, podcast_id: str): + url = f"{BASE_URL}/podcasts/{podcast_id}" + params = {"sort": "recent_first"} + response = requests.get(url, headers=self._headers(), params=params, timeout=10) + response.raise_for_status() + data = response.json() + return data.get("episodes", []) + # ------------------------------------------------------------------------- + # Playback + # ------------------------------------------------------------------------- + + async def play_episode(self, episode: dict, state: dict): + state["current_episode"] = episode + title = episode["title"] + podcast = episode["podcast"]["title_original"] + audio_url = episode.get("audio") + + if not audio_url: + await self.capability_worker.speak("No audio URL found for this episode.") + return + + await self.capability_worker.speak(f"Playing {title} from {podcast}.") + + # --- Streaming long audio --- + await self.capability_worker.stream_init() + try: + with requests.get(audio_url, stream=True, timeout=10) as r: + r.raise_for_status() + for chunk in r.iter_content(chunk_size=4096): + if chunk: + await self.capability_worker.send_audio_data_in_stream(chunk) + except Exception as e: + await self.capability_worker.speak(f"Error streaming audio: {e}") + finally: + await self.capability_worker.stream_end() + # ------------------------------------------------------------------------- + # Main Flow + # ------------------------------------------------------------------------- + async def run(self): + try: + state = { + "results": [], + "current_episode": None + } + + await self.capability_worker.speak( + "What would you like to listen to? " + "You can search for an episode, a podcast, or say surprise me." + ) + + while True: + user_input = await self.capability_worker.user_response() + if not user_input: + continue + + text = user_input.lower() + + # ---------------- EXIT ---------------- + if self._wants(text, EXIT_WORDS): + await self.capability_worker.speak("Stopping playback.") + break + + # ---------------- RANDOM ---------------- + if self._wants(text, SURPRISE_WORDS): + ep = self.random_episode() + await self.play_episode(ep, state) + continue + + # ---------------- WHAT'S PLAYING ---------------- + if self._wants(text, WHATS_PLAYING_WORDS): + ep = state.get("current_episode") + if ep: + await self.capability_worker.speak( + f"You're listening to {ep['title']} " + f"from {ep['podcast']['title_original']}." + ) + else: + await self.capability_worker.speak("Nothing is playing right now.") + continue + + # ---------------- PODCAST FLOW ---------------- + if "podcast" in text: + + await self.capability_worker.speak("What podcast are you looking for?") + query = await self.capability_worker.user_response() + if not query: + continue + + podcasts = self.search_podcasts(query) + + if not podcasts: + await self.capability_worker.speak("No podcasts found.") + continue + + for i, p in enumerate(podcasts[:3], start=1): + await self.capability_worker.speak( + f"{i}. {p['title_original']} by {p['publisher_original']}." + ) + + await self.capability_worker.speak( + "Choose 1, 2, or 3." + ) + + choice = await self.capability_worker.user_response() + if not choice: + continue + + index_map = {"1": 0, "2": 1, "3": 2, + "first": 0, "second": 1, "third": 2} + + selected_index = None + for key, value in index_map.items(): + if key in choice.lower(): + selected_index = value + break + + if selected_index is None or selected_index >= len(podcasts): + continue + + selected_podcast = podcasts[selected_index] + + episodes = self.get_podcast_episodes(selected_podcast["id"]) + if not episodes: + await self.capability_worker.speak("No episodes found.") + continue + + latest_five = episodes[:5] + + await self.capability_worker.speak( + f"Here are the latest five episodes of {selected_podcast['title']}:" + ) + + for i, ep in enumerate(latest_five, start=1): + await self.capability_worker.speak( + f"{i}. {ep['title']}." + ) + + await self.capability_worker.speak( + "Choose 1, 2, 3, 4, or 5." + ) + + ep_choice = await self.capability_worker.user_response() + if not ep_choice: + continue + + ep_index_map = { + "1": 0, "2": 1, "3": 2, "4": 3, "5": 4, + "first": 0, "second": 1, "third": 2, + "fourth": 3, "fifth": 4 + } + + selected_ep_index = None + for key, value in ep_index_map.items(): + if key in ep_choice.lower(): + selected_ep_index = value + break + + if selected_ep_index is None or selected_ep_index >= len(latest_five): + continue + + await self.play_episode(latest_five[selected_ep_index], state) + continue + + # ---------------- EPISODE SEARCH FLOW ---------------- + if self._wants(text, SEARCH_WORDS): + + results = self.search_episodes(user_input) + + if not results: + await self.capability_worker.speak( + "I couldn't find any episodes for that." + ) + continue + + state["results"] = results + + await self.capability_worker.speak("Here are a few options:") + + for i, ep in enumerate(results[:3], start=1): + audio_sec = ep.get("audio_length_sec") + if audio_sec: + minutes = int(audio_sec // 60) + duration = f"{minutes} minutes" + else: + duration = "unknown duration" + + await self.capability_worker.speak( + f"{i}. {ep['title']} " + f"from {ep['podcast']['title']}, {duration}." + ) + + await self.capability_worker.speak( + "Choose 1, 2, or 3." + ) + + choice = await self.capability_worker.user_response() + if not choice: + continue + + for key, index in {"1": 0, "2": 1, "3": 2, + "first": 0, "second": 1, "third": 2}.items(): + if key in choice.lower(): + if index < len(results): + await self.play_episode(results[index], state) + break + + continue + + except Exception as e: + self.worker.editor_logging_handler.error(f"[PodcastPlayer] Error: {e}") + await self.capability_worker.speak( + "Something went wrong while playing the podcast." + ) + + self.capability_worker.resume_normal_flow() + \ No newline at end of file From f0dc0ff033aa0529d1fe9239078a49cb13bf8d48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 13:01:51 +0000 Subject: [PATCH 2/4] style: auto-format Python files with autoflake + autopep8 --- community/podcast-player/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/community/podcast-player/main.py b/community/podcast-player/main.py index 7f63fbba..06bf9491 100644 --- a/community/podcast-player/main.py +++ b/community/podcast-player/main.py @@ -58,7 +58,7 @@ def random_episode(self): response = requests.get(url, headers=self._headers(), timeout=10) response.raise_for_status() return response.json() - + def get_podcast_episodes(self, podcast_id: str): url = f"{BASE_URL}/podcasts/{podcast_id}" params = {"sort": "recent_first"} @@ -97,6 +97,7 @@ async def play_episode(self, episode: dict, state: dict): # ------------------------------------------------------------------------- # Main Flow # ------------------------------------------------------------------------- + async def run(self): try: state = { @@ -167,7 +168,7 @@ async def run(self): continue index_map = {"1": 0, "2": 1, "3": 2, - "first": 0, "second": 1, "third": 2} + "first": 0, "second": 1, "third": 2} selected_index = None for key, value in index_map.items(): @@ -259,7 +260,7 @@ async def run(self): continue for key, index in {"1": 0, "2": 1, "3": 2, - "first": 0, "second": 1, "third": 2}.items(): + "first": 0, "second": 1, "third": 2}.items(): if key in choice.lower(): if index < len(results): await self.play_episode(results[index], state) @@ -274,4 +275,3 @@ async def run(self): ) self.capability_worker.resume_normal_flow() - \ No newline at end of file From 095bbf17b6b94ea9734ea9d6ede7353b437b871c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=A1=D0=B0=D0=BF=D0=BA?= =?UTF-8?q?=D0=BE?= Date: Fri, 27 Mar 2026 18:39:36 +0200 Subject: [PATCH 3/4] update podcast_player with llm --- community/podcast-player/main.py | 210 ++++++++++++++++++------------- 1 file changed, 120 insertions(+), 90 deletions(-) diff --git a/community/podcast-player/main.py b/community/podcast-player/main.py index 7f63fbba..339e2bbb 100644 --- a/community/podcast-player/main.py +++ b/community/podcast-player/main.py @@ -1,3 +1,5 @@ +import json + import requests from src.agent.capability import MatchingCapability from src.main import AgentWorker @@ -6,7 +8,13 @@ API_KEY = "YOUR_LISTEN_NOTES_API_KEY" # YOUR KEY from https://www.listennotes.com/api/dashboard/#apps BASE_URL = "https://listen-api.listennotes.com/api/v2" -EXIT_WORDS = {"stop", "pause", "exit", "quit", "cancel"} +EXIT_WORDS = { + "stop", "exit", "quit", "cancel", + "forget it", "never mind", "nevermind", "done", + "bye", "that's all", "no thanks", "actually", "leave it" +} + +PAUSE_WORDS = {"pause", "hold on", "wait"} SURPRISE_WORDS = {"surprise", "random", "anything"} SEARCH_WORDS = {"find", "search", "podcast", "listen"} ELSE_WORDS = {"something else", "another one"} @@ -34,11 +42,33 @@ def _headers(self): def _wants(self, text: str, words: set[str]) -> bool: t = text.lower() return any(w in t for w in words) - + # ------------------------------------------------------------------------- # API Calls # ------------------------------------------------------------------------- - + + async def classify_intent(self, user_input: str) -> dict: + # Надсилаємо user_input LLM і отримуємо відповідь (можеш інтегрувати OpenAI GPT) + prompt = f""" + Classify the user's command into one of the following intents: + - play_podcast: user wants to play a podcast by name or topic + - play_episode: user wants to play a specific episode + - play_random: user wants to play a random episode + - pause: user wants to pause the playback + - exit: user wants to stop the ability + - whats_playing: user asks what is currently playing + Respond in JSON: {{ "intent": "...", "query": "..." }} + User said: "{user_input}" + """ + llm_response = self.capability_worker.text_to_text_response(prompt_text=prompt) + + try: + data = json.loads(llm_response) + return data + except Exception: + # fallback + return {"intent": "unknown", "query": None} + def search_episodes(self, query: str): url = f"{BASE_URL}/search" params = {"q": query, "type": "episode", "sort_by_date": 0, "page_size": 5} @@ -69,7 +99,15 @@ def get_podcast_episodes(self, podcast_id: str): # ------------------------------------------------------------------------- # Playback # ------------------------------------------------------------------------- + def match_choice(self, user_input: str, options: list[dict], key: str): + text = user_input.lower() + for opt in options: + if opt[key].lower() in text: + return opt + + return options[0] if options else None + async def play_episode(self, episode: dict, state: dict): state["current_episode"] = episode title = episode["title"] @@ -91,7 +129,7 @@ async def play_episode(self, episode: dict, state: dict): if chunk: await self.capability_worker.send_audio_data_in_stream(chunk) except Exception as e: - await self.capability_worker.speak(f"Error streaming audio: {e}") + await self.capability_worker.speak("Had trouble loading that episode. Want to try a different one?") finally: await self.capability_worker.stream_end() # ------------------------------------------------------------------------- @@ -105,8 +143,7 @@ async def run(self): } await self.capability_worker.speak( - "What would you like to listen to? " - "You can search for an episode, a podcast, or say surprise me." + "What do you want to listen to?" ) while True: @@ -114,21 +151,26 @@ async def run(self): if not user_input: continue - text = user_input.lower() + result = await self.classify_intent(user_input) + intent = result.get("intent") + query = result.get("query") # ---------------- EXIT ---------------- - if self._wants(text, EXIT_WORDS): + if intent == "exit": await self.capability_worker.speak("Stopping playback.") break - + + elif intent == "pause": + await self.capability_worker.speak("Paused.") + continue # ---------------- RANDOM ---------------- - if self._wants(text, SURPRISE_WORDS): + elif intent == "play_random": ep = self.random_episode() await self.play_episode(ep, state) continue # ---------------- WHAT'S PLAYING ---------------- - if self._wants(text, WHATS_PLAYING_WORDS): + elif intent == "whats_playing": ep = state.get("current_episode") if ep: await self.capability_worker.speak( @@ -140,90 +182,75 @@ async def run(self): continue # ---------------- PODCAST FLOW ---------------- - if "podcast" in text: - - await self.capability_worker.speak("What podcast are you looking for?") - query = await self.capability_worker.user_response() - if not query: - continue - - podcasts = self.search_podcasts(query) - - if not podcasts: - await self.capability_worker.speak("No podcasts found.") - continue + elif intent == "play_podcast": + if not query: + await self.capability_worker.speak("Which podcast do you want to listen to?") + query = await self.capability_worker.user_response() + if not query: + continue + + podcasts = self.search_podcasts(query) + + if not podcasts: + await self.capability_worker.speak( + "Couldn't find any podcasts for that — try another search?" + ) + continue + + # ---- TOP PODCASTS ---- + top = podcasts[:3] + titles = [p["title_original"] for p in top] + + if len(titles) == 1: + spoken_list = titles[0] + else: + spoken_list = f"{', '.join(titles[:-1])}, and {titles[-1]}" - for i, p in enumerate(podcasts[:3], start=1): await self.capability_worker.speak( - f"{i}. {p['title_original']} by {p['publisher_original']}." + f"I found a few podcasts: {spoken_list}. Which one sounds right?" ) - await self.capability_worker.speak( - "Choose 1, 2, or 3." - ) - - choice = await self.capability_worker.user_response() - if not choice: - continue - - index_map = {"1": 0, "2": 1, "3": 2, - "first": 0, "second": 1, "third": 2} - - selected_index = None - for key, value in index_map.items(): - if key in choice.lower(): - selected_index = value - break + choice = await self.capability_worker.user_response() + if not choice: + continue - if selected_index is None or selected_index >= len(podcasts): - continue + # ---- MATCH PODCAST ---- + selected_podcast = self.match_choice(choice, top, "title_original") - selected_podcast = podcasts[selected_index] + # ---- GET EPISODES ---- + episodes = self.get_podcast_episodes(selected_podcast["id"]) - episodes = self.get_podcast_episodes(selected_podcast["id"]) - if not episodes: - await self.capability_worker.speak("No episodes found.") - continue + if not episodes: + await self.capability_worker.speak( + "That podcast doesn't seem to have any episodes available." + ) + continue - latest_five = episodes[:5] + latest_five = episodes[:5] + ep_titles = [ep["title"] for ep in latest_five] - await self.capability_worker.speak( - f"Here are the latest five episodes of {selected_podcast['title']}:" - ) + if len(ep_titles) == 1: + spoken_eps = ep_titles[0] + else: + spoken_eps = f"{', '.join(ep_titles[:-1])}, and {ep_titles[-1]}" - for i, ep in enumerate(latest_five, start=1): await self.capability_worker.speak( - f"{i}. {ep['title']}." + f"Latest episodes from {selected_podcast['title_original']} include: {spoken_eps}. " + "Which one do you want?" ) - await self.capability_worker.speak( - "Choose 1, 2, 3, 4, or 5." - ) - - ep_choice = await self.capability_worker.user_response() - if not ep_choice: - continue + ep_choice = await self.capability_worker.user_response() + if not ep_choice: + continue - ep_index_map = { - "1": 0, "2": 1, "3": 2, "4": 3, "5": 4, - "first": 0, "second": 1, "third": 2, - "fourth": 3, "fifth": 4 - } + # ---- MATCH EPISODE ---- + selected_episode = self.match_choice(ep_choice, latest_five, "title") - selected_ep_index = None - for key, value in ep_index_map.items(): - if key in ep_choice.lower(): - selected_ep_index = value - break - - if selected_ep_index is None or selected_ep_index >= len(latest_five): + await self.play_episode(selected_episode, state) continue - - await self.play_episode(latest_five[selected_ep_index], state) - continue - + # ---------------- EPISODE SEARCH FLOW ---------------- - if self._wants(text, SEARCH_WORDS): + elif intent == "play_episode": results = self.search_episodes(user_input) @@ -237,7 +264,10 @@ async def run(self): await self.capability_worker.speak("Here are a few options:") - for i, ep in enumerate(results[:3], start=1): + top = results[:3] + + titles = [] + for ep in top: audio_sec = ep.get("audio_length_sec") if audio_sec: minutes = int(audio_sec // 60) @@ -245,28 +275,28 @@ async def run(self): else: duration = "unknown duration" - await self.capability_worker.speak( - f"{i}. {ep['title']} " - f"from {ep['podcast']['title']}, {duration}." - ) + titles.append(f"{ep['title']} from {ep['podcast']['title']} ({duration})") + + # ---- Natural sentence ---- + if len(titles) == 1: + spoken = titles[0] + else: + spoken = f"{', '.join(titles[:-1])}, and {titles[-1]}" await self.capability_worker.speak( - "Choose 1, 2, or 3." + f"I found a few episodes: {spoken}. Which one sounds good?" ) choice = await self.capability_worker.user_response() if not choice: continue - for key, index in {"1": 0, "2": 1, "3": 2, - "first": 0, "second": 1, "third": 2}.items(): - if key in choice.lower(): - if index < len(results): - await self.play_episode(results[index], state) - break + selected_episode = self.match_choice(choice, top, "title") + await self.play_episode(selected_episode, state) continue + except Exception as e: self.worker.editor_logging_handler.error(f"[PodcastPlayer] Error: {e}") await self.capability_worker.speak( From 9502dd2e63d9b2702326aae0ca466b547e62825e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 16:41:38 +0000 Subject: [PATCH 4/4] style: auto-format Python files with autoflake + autopep8 --- community/podcast-player/main.py | 116 +++++++++++++++---------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/community/podcast-player/main.py b/community/podcast-player/main.py index 339e2bbb..fd36b68a 100644 --- a/community/podcast-player/main.py +++ b/community/podcast-player/main.py @@ -42,11 +42,11 @@ def _headers(self): def _wants(self, text: str, words: set[str]) -> bool: t = text.lower() return any(w in t for w in words) - + # ------------------------------------------------------------------------- # API Calls # ------------------------------------------------------------------------- - + async def classify_intent(self, user_input: str) -> dict: # Надсилаємо user_input LLM і отримуємо відповідь (можеш інтегрувати OpenAI GPT) prompt = f""" @@ -61,14 +61,14 @@ async def classify_intent(self, user_input: str) -> dict: User said: "{user_input}" """ llm_response = self.capability_worker.text_to_text_response(prompt_text=prompt) - + try: data = json.loads(llm_response) return data except Exception: # fallback return {"intent": "unknown", "query": None} - + def search_episodes(self, query: str): url = f"{BASE_URL}/search" params = {"q": query, "type": "episode", "sort_by_date": 0, "page_size": 5} @@ -88,7 +88,7 @@ def random_episode(self): response = requests.get(url, headers=self._headers(), timeout=10) response.raise_for_status() return response.json() - + def get_podcast_episodes(self, podcast_id: str): url = f"{BASE_URL}/podcasts/{podcast_id}" params = {"sort": "recent_first"} @@ -99,6 +99,7 @@ def get_podcast_episodes(self, podcast_id: str): # ------------------------------------------------------------------------- # Playback # ------------------------------------------------------------------------- + def match_choice(self, user_input: str, options: list[dict], key: str): text = user_input.lower() @@ -107,7 +108,7 @@ def match_choice(self, user_input: str, options: list[dict], key: str): return opt return options[0] if options else None - + async def play_episode(self, episode: dict, state: dict): state["current_episode"] = episode title = episode["title"] @@ -128,13 +129,14 @@ async def play_episode(self, episode: dict, state: dict): for chunk in r.iter_content(chunk_size=4096): if chunk: await self.capability_worker.send_audio_data_in_stream(chunk) - except Exception as e: + except Exception: await self.capability_worker.speak("Had trouble loading that episode. Want to try a different one?") finally: await self.capability_worker.stream_end() # ------------------------------------------------------------------------- # Main Flow # ------------------------------------------------------------------------- + async def run(self): try: state = { @@ -159,10 +161,10 @@ async def run(self): if intent == "exit": await self.capability_worker.speak("Stopping playback.") break - + elif intent == "pause": await self.capability_worker.speak("Paused.") - continue + continue # ---------------- RANDOM ---------------- elif intent == "play_random": ep = self.random_episode() @@ -183,72 +185,72 @@ async def run(self): # ---------------- PODCAST FLOW ---------------- elif intent == "play_podcast": + if not query: + await self.capability_worker.speak("Which podcast do you want to listen to?") + query = await self.capability_worker.user_response() if not query: - await self.capability_worker.speak("Which podcast do you want to listen to?") - query = await self.capability_worker.user_response() - if not query: - continue - - podcasts = self.search_podcasts(query) - - if not podcasts: - await self.capability_worker.speak( - "Couldn't find any podcasts for that — try another search?" - ) continue - # ---- TOP PODCASTS ---- - top = podcasts[:3] - titles = [p["title_original"] for p in top] - - if len(titles) == 1: - spoken_list = titles[0] - else: - spoken_list = f"{', '.join(titles[:-1])}, and {titles[-1]}" + podcasts = self.search_podcasts(query) + if not podcasts: await self.capability_worker.speak( - f"I found a few podcasts: {spoken_list}. Which one sounds right?" + "Couldn't find any podcasts for that — try another search?" ) + continue - choice = await self.capability_worker.user_response() - if not choice: - continue + # ---- TOP PODCASTS ---- + top = podcasts[:3] + titles = [p["title_original"] for p in top] - # ---- MATCH PODCAST ---- - selected_podcast = self.match_choice(choice, top, "title_original") + if len(titles) == 1: + spoken_list = titles[0] + else: + spoken_list = f"{', '.join(titles[:-1])}, and {titles[-1]}" - # ---- GET EPISODES ---- - episodes = self.get_podcast_episodes(selected_podcast["id"]) + await self.capability_worker.speak( + f"I found a few podcasts: {spoken_list}. Which one sounds right?" + ) - if not episodes: - await self.capability_worker.speak( - "That podcast doesn't seem to have any episodes available." - ) - continue + choice = await self.capability_worker.user_response() + if not choice: + continue - latest_five = episodes[:5] - ep_titles = [ep["title"] for ep in latest_five] + # ---- MATCH PODCAST ---- + selected_podcast = self.match_choice(choice, top, "title_original") - if len(ep_titles) == 1: - spoken_eps = ep_titles[0] - else: - spoken_eps = f"{', '.join(ep_titles[:-1])}, and {ep_titles[-1]}" + # ---- GET EPISODES ---- + episodes = self.get_podcast_episodes(selected_podcast["id"]) + if not episodes: await self.capability_worker.speak( - f"Latest episodes from {selected_podcast['title_original']} include: {spoken_eps}. " - "Which one do you want?" + "That podcast doesn't seem to have any episodes available." ) + continue - ep_choice = await self.capability_worker.user_response() - if not ep_choice: - continue + latest_five = episodes[:5] + ep_titles = [ep["title"] for ep in latest_five] + + if len(ep_titles) == 1: + spoken_eps = ep_titles[0] + else: + spoken_eps = f"{', '.join(ep_titles[:-1])}, and {ep_titles[-1]}" - # ---- MATCH EPISODE ---- - selected_episode = self.match_choice(ep_choice, latest_five, "title") + await self.capability_worker.speak( + f"Latest episodes from {selected_podcast['title_original']} include: {spoken_eps}. " + "Which one do you want?" + ) - await self.play_episode(selected_episode, state) + ep_choice = await self.capability_worker.user_response() + if not ep_choice: continue - + + # ---- MATCH EPISODE ---- + selected_episode = self.match_choice(ep_choice, latest_five, "title") + + await self.play_episode(selected_episode, state) + continue + # ---------------- EPISODE SEARCH FLOW ---------------- elif intent == "play_episode": @@ -296,7 +298,6 @@ async def run(self): await self.play_episode(selected_episode, state) continue - except Exception as e: self.worker.editor_logging_handler.error(f"[PodcastPlayer] Error: {e}") await self.capability_worker.speak( @@ -304,4 +305,3 @@ async def run(self): ) self.capability_worker.resume_normal_flow() - \ No newline at end of file