Nikita Makarov commited on
Commit
5e41eee
·
1 Parent(s): 27ca72c
run.py CHANGED
@@ -12,7 +12,7 @@ from app import demo
12
  if __name__ == "__main__":
13
  demo.launch(
14
  server_name="0.0.0.0",
15
- server_port=7867,
16
  share=False
17
  )
18
 
 
12
  if __name__ == "__main__":
13
  demo.launch(
14
  server_name="0.0.0.0",
15
+ server_port=7870,
16
  share=False
17
  )
18
 
src/app.py CHANGED
@@ -6,6 +6,7 @@ import time
6
  import re
7
  from typing import Dict, Any, List, Optional
8
  import threading
 
9
 
10
  # Get project root directory (parent of src/)
11
  PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -415,12 +416,34 @@ def play_next_segment():
415
  youtube_id = track["url"].split("v=")[-1].split("&")[0]
416
 
417
  if youtube_id:
418
- # Simple YouTube iframe embed with autoplay
 
 
 
 
 
 
 
 
 
 
 
419
  music_player_html = f"""
 
 
 
 
 
 
 
 
 
 
 
420
  <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
421
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎵 {track.get('title', 'Unknown')}</h4>
422
  <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
423
- <div style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
424
  <iframe
425
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
426
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
@@ -433,7 +456,7 @@ def play_next_segment():
433
  </p>
434
  </div>
435
  """
436
- print(f"✅ YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id})")
437
  else:
438
  # Fallback: Just show link
439
  music_player_html = f"""
@@ -503,12 +526,34 @@ def play_next_segment():
503
  if podcast.get("source") == "youtube" and podcast.get("youtube_id"):
504
  youtube_id = podcast.get("youtube_id", "")
505
 
506
- # Use simple iframe embed (Gradio sanitizes script tags)
 
 
 
 
 
 
 
 
 
 
 
507
  music_player_html = f"""
 
 
 
 
 
 
 
 
 
 
 
508
  <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
509
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎙️ {podcast.get('title', 'Podcast')}</h4>
510
  <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
511
- <div style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
512
  <iframe
513
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
514
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
@@ -521,7 +566,7 @@ def play_next_segment():
521
  </p>
522
  </div>
523
  """
524
- print(f"✅ Podcast YouTube player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id})")
525
  elif podcast.get("url") and "youtube" in podcast.get("url", ""):
526
  # Fallback: extract YouTube ID from URL and create simple embed
527
  url = podcast.get("url", "")
@@ -532,11 +577,32 @@ def play_next_segment():
532
  youtube_id = url.split("youtu.be/")[-1].split("?")[0]
533
 
534
  if youtube_id:
 
 
 
 
 
 
 
 
 
 
535
  music_player_html = f"""
 
 
 
 
 
 
 
 
 
 
 
536
  <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
537
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎙️ {podcast.get('title', 'Podcast')}</h4>
538
  <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
539
- <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; border-radius: 8px;">
540
  <iframe
541
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
542
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
@@ -549,7 +615,7 @@ def play_next_segment():
549
  </p>
550
  </div>
551
  """
552
- print(f"✅ Podcast iframe player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id})")
553
 
554
  # Progress info
555
  progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
@@ -783,12 +849,34 @@ def handle_voice_request():
783
  youtube_id = track["url"].split("v=")[-1].split("&")[0]
784
 
785
  if youtube_id:
786
- # YouTube embed with direct iframe (Gradio sanitizes script tags)
 
 
 
 
 
 
 
 
 
 
 
787
  music_player_html = f"""
 
 
 
 
 
 
 
 
 
 
 
788
  <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
789
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎵 {track.get('title', 'Unknown')}</h4>
790
  <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
791
- <div style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
792
  <iframe
793
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
794
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
@@ -801,6 +889,7 @@ def handle_voice_request():
801
  </p>
802
  </div>
803
  """
 
804
  print(f"✅ YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id})")
805
  else:
806
  music_player_html = f"""
@@ -1520,7 +1609,7 @@ Your passphrase: **{passphrase}**
1520
  if __name__ == "__main__":
1521
  demo.launch(
1522
  server_name="0.0.0.0",
1523
- server_port=7867,
1524
  share=False
1525
  )
1526
 
 
6
  import re
7
  from typing import Dict, Any, List, Optional
8
  import threading
9
+ from pydub import AudioSegment
10
 
11
  # Get project root directory (parent of src/)
12
  PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
416
  youtube_id = track["url"].split("v=")[-1].split("&")[0]
417
 
418
  if youtube_id:
419
+ # Get host audio duration to calculate delay
420
+ delay_ms = 0
421
+ if host_audio_file and os.path.exists(host_audio_file):
422
+ try:
423
+ audio = AudioSegment.from_file(host_audio_file)
424
+ duration_ms = len(audio)
425
+ delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
426
+ print(f"⏱️ Host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
427
+ except Exception as e:
428
+ print(f"⚠️ Could not get audio duration: {e}")
429
+
430
+ # YouTube iframe with delayed display via CSS animation
431
  music_player_html = f"""
432
+ <style>
433
+ @keyframes showYoutube {{
434
+ from {{ opacity: 0; }}
435
+ to {{ opacity: 1; }}
436
+ }}
437
+ .youtube-delayed {{
438
+ animation: showYoutube 0.5s ease-in forwards;
439
+ animation-delay: {delay_ms}ms;
440
+ opacity: 0;
441
+ }}
442
+ </style>
443
  <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
444
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎵 {track.get('title', 'Unknown')}</h4>
445
  <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
446
+ <div class="youtube-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
447
  <iframe
448
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
449
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
 
456
  </p>
457
  </div>
458
  """
459
+ print(f"✅ YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
460
  else:
461
  # Fallback: Just show link
462
  music_player_html = f"""
 
526
  if podcast.get("source") == "youtube" and podcast.get("youtube_id"):
527
  youtube_id = podcast.get("youtube_id", "")
528
 
529
+ # Get host audio duration to calculate delay
530
+ delay_ms = 0
531
+ if host_audio_file and os.path.exists(host_audio_file):
532
+ try:
533
+ audio = AudioSegment.from_file(host_audio_file)
534
+ duration_ms = len(audio)
535
+ delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
536
+ print(f"⏱️ Podcast host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
537
+ except Exception as e:
538
+ print(f"⚠️ Could not get audio duration: {e}")
539
+
540
+ # YouTube iframe with delayed display
541
  music_player_html = f"""
542
+ <style>
543
+ @keyframes showPodcast {{
544
+ from {{ opacity: 0; }}
545
+ to {{ opacity: 1; }}
546
+ }}
547
+ .podcast-delayed {{
548
+ animation: showPodcast 0.5s ease-in forwards;
549
+ animation-delay: {delay_ms}ms;
550
+ opacity: 0;
551
+ }}
552
+ </style>
553
  <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
554
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎙️ {podcast.get('title', 'Podcast')}</h4>
555
  <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
556
+ <div class="podcast-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
557
  <iframe
558
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
559
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
 
566
  </p>
567
  </div>
568
  """
569
+ print(f"✅ Podcast YouTube player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
570
  elif podcast.get("url") and "youtube" in podcast.get("url", ""):
571
  # Fallback: extract YouTube ID from URL and create simple embed
572
  url = podcast.get("url", "")
 
577
  youtube_id = url.split("youtu.be/")[-1].split("?")[0]
578
 
579
  if youtube_id:
580
+ # Get host audio duration to calculate delay
581
+ delay_ms = 0
582
+ if host_audio_file and os.path.exists(host_audio_file):
583
+ try:
584
+ audio = AudioSegment.from_file(host_audio_file)
585
+ duration_ms = len(audio)
586
+ delay_ms = max(0, duration_ms - 3000)
587
+ except:
588
+ pass
589
+
590
  music_player_html = f"""
591
+ <style>
592
+ @keyframes showPodcast2 {{
593
+ from {{ opacity: 0; }}
594
+ to {{ opacity: 1; }}
595
+ }}
596
+ .podcast-delayed-2 {{
597
+ animation: showPodcast2 0.5s ease-in forwards;
598
+ animation-delay: {delay_ms}ms;
599
+ opacity: 0;
600
+ }}
601
+ </style>
602
  <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
603
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎙️ {podcast.get('title', 'Podcast')}</h4>
604
  <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
605
+ <div class="podcast-delayed-2" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; border-radius: 8px;">
606
  <iframe
607
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
608
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
 
615
  </p>
616
  </div>
617
  """
618
+ print(f"✅ Podcast iframe player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
619
 
620
  # Progress info
621
  progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
 
849
  youtube_id = track["url"].split("v=")[-1].split("&")[0]
850
 
851
  if youtube_id:
852
+ # Get host audio duration to calculate delay
853
+ delay_ms = 0
854
+ if audio_file and os.path.exists(audio_file):
855
+ try:
856
+ audio = AudioSegment.from_file(audio_file)
857
+ duration_ms = len(audio)
858
+ delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
859
+ print(f"⏱️ Voice request audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
860
+ except Exception as e:
861
+ print(f"⚠️ Could not get audio duration: {e}")
862
+
863
+ # YouTube embed with delayed display
864
  music_player_html = f"""
865
+ <style>
866
+ @keyframes showVoiceYoutube {{
867
+ from {{ opacity: 0; }}
868
+ to {{ opacity: 1; }}
869
+ }}
870
+ .voice-youtube-delayed {{
871
+ animation: showVoiceYoutube 0.5s ease-in forwards;
872
+ animation-delay: {delay_ms}ms;
873
+ opacity: 0;
874
+ }}
875
+ </style>
876
  <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
877
  <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎵 {track.get('title', 'Unknown')}</h4>
878
  <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
879
+ <div class="voice-youtube-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
880
  <iframe
881
  style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
882
  src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
 
889
  </p>
890
  </div>
891
  """
892
+ print(f"✅ Voice request YouTube player created - shows after {delay_ms}ms")
893
  print(f"✅ YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id})")
894
  else:
895
  music_player_html = f"""
 
1609
  if __name__ == "__main__":
1610
  demo.launch(
1611
  server_name="0.0.0.0",
1612
+ server_port=7870,
1613
  share=False
1614
  )
1615
 
src/mcp_servers/music_server.py CHANGED
@@ -24,8 +24,78 @@ class MusicMCPServer:
24
  self.description = "Provides music recommendations and free music tracks"
25
  self.cache_dir = "music_cache"
26
  os.makedirs(self.cache_dir, exist_ok=True)
 
 
 
 
 
 
27
 
28
- def search_youtube_music(self, query: str, limit: int = 5, fast: bool = False) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  """
30
  Search for free music on YouTube
31
 
@@ -33,6 +103,7 @@ class MusicMCPServer:
33
  query: Search query (e.g., "pop music", "jazz instrumental", "song name")
34
  limit: Number of results to fetch (will randomly select one)
35
  fast: If True, use flat extraction for faster results (less metadata)
 
36
 
37
  Returns:
38
  List of track dictionaries with YouTube URLs
@@ -47,8 +118,8 @@ class MusicMCPServer:
47
  'default_search': 'ytsearch',
48
  }
49
 
50
- # Search for more results to allow random selection
51
- search_limit = max(limit, 10) # Get at least 10 for variety
52
 
53
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
54
  # Don't add "music" if query already contains it or is specific
@@ -84,10 +155,18 @@ class MusicMCPServer:
84
  if len(valid_entries) > 1:
85
  random.shuffle(valid_entries)
86
 
87
- # Take requested limit from shuffled results
88
- for entry in valid_entries[:limit]:
 
 
 
89
  video_id = entry.get('id') or entry.get('url', '')
90
  if video_id:
 
 
 
 
 
91
  track = {
92
  "title": entry.get('title', 'Unknown'),
93
  "artist": entry.get('uploader', entry.get('channel', 'Unknown Artist')),
@@ -98,7 +177,7 @@ class MusicMCPServer:
98
  "source": "youtube"
99
  }
100
  tracks.append(track)
101
- print(f" ✓ Found: {track['title']} by {track['artist']}")
102
 
103
  except Exception as e:
104
  print(f"❌ Error searching YouTube: {e}")
 
24
  self.description = "Provides music recommendations and free music tracks"
25
  self.cache_dir = "music_cache"
26
  os.makedirs(self.cache_dir, exist_ok=True)
27
+ # Cache for embed check results to avoid repeated API calls
28
+ self._embed_cache = {}
29
+
30
+ def check_video_embeddable(self, video_id: str) -> bool:
31
+ """
32
+ Check if a YouTube video is available and embeddable.
33
 
34
+ Args:
35
+ video_id: YouTube video ID
36
+
37
+ Returns:
38
+ True if video is embeddable, False otherwise
39
+ """
40
+ # Check cache first
41
+ if video_id in self._embed_cache:
42
+ return self._embed_cache[video_id]
43
+
44
+ try:
45
+ ydl_opts = {
46
+ 'quiet': True,
47
+ 'no_warnings': True,
48
+ 'skip_download': True,
49
+ }
50
+
51
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
52
+ url = f"https://www.youtube.com/watch?v={video_id}"
53
+ info = ydl.extract_info(url, download=False)
54
+
55
+ if not info:
56
+ print(f"⚠️ Video {video_id}: not found")
57
+ self._embed_cache[video_id] = False
58
+ return False
59
+
60
+ # Check if video is available
61
+ availability = info.get('availability')
62
+ if availability and availability != 'public':
63
+ print(f"⚠️ Video {video_id}: not public ({availability})")
64
+ self._embed_cache[video_id] = False
65
+ return False
66
+
67
+ # Check if video is age-restricted (often blocks embedding)
68
+ if info.get('age_limit', 0) > 0:
69
+ print(f"⚠️ Video {video_id}: age restricted")
70
+ self._embed_cache[video_id] = False
71
+ return False
72
+
73
+ # Check if video is live (live streams might have issues)
74
+ if info.get('is_live'):
75
+ print(f"ℹ️ Video {video_id}: live stream (may have embed issues)")
76
+ # Allow live streams but note they might have issues
77
+
78
+ # Check playability
79
+ playable_in_embed = info.get('playable_in_embed', True)
80
+ if playable_in_embed is False:
81
+ print(f"⚠️ Video {video_id}: not playable in embed")
82
+ self._embed_cache[video_id] = False
83
+ return False
84
+
85
+ print(f"✅ Video {video_id}: embeddable")
86
+ self._embed_cache[video_id] = True
87
+ return True
88
+
89
+ except Exception as e:
90
+ error_msg = str(e).lower()
91
+ if 'unavailable' in error_msg or 'private' in error_msg or 'removed' in error_msg:
92
+ print(f"⚠️ Video {video_id}: unavailable - {e}")
93
+ else:
94
+ print(f"⚠️ Video {video_id}: check failed - {e}")
95
+ self._embed_cache[video_id] = False
96
+ return False
97
+
98
+ def search_youtube_music(self, query: str, limit: int = 5, fast: bool = False, check_embed: bool = True) -> List[Dict[str, Any]]:
99
  """
100
  Search for free music on YouTube
101
 
 
103
  query: Search query (e.g., "pop music", "jazz instrumental", "song name")
104
  limit: Number of results to fetch (will randomly select one)
105
  fast: If True, use flat extraction for faster results (less metadata)
106
+ check_embed: If True, verify videos are embeddable (recommended)
107
 
108
  Returns:
109
  List of track dictionaries with YouTube URLs
 
118
  'default_search': 'ytsearch',
119
  }
120
 
121
+ # Search for more results to allow for filtering and random selection
122
+ search_limit = max(limit * 3, 15) if check_embed else max(limit, 10)
123
 
124
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
125
  # Don't add "music" if query already contains it or is specific
 
155
  if len(valid_entries) > 1:
156
  random.shuffle(valid_entries)
157
 
158
+ # Check embeddability and take requested limit
159
+ for entry in valid_entries:
160
+ if len(tracks) >= limit:
161
+ break
162
+
163
  video_id = entry.get('id') or entry.get('url', '')
164
  if video_id:
165
+ # Check if video is embeddable
166
+ if check_embed and not self.check_video_embeddable(video_id):
167
+ print(f" ✗ Skipping non-embeddable: {entry.get('title', 'Unknown')}")
168
+ continue
169
+
170
  track = {
171
  "title": entry.get('title', 'Unknown'),
172
  "artist": entry.get('uploader', entry.get('channel', 'Unknown Artist')),
 
177
  "source": "youtube"
178
  }
179
  tracks.append(track)
180
+ print(f" ✓ Found embeddable: {track['title']} by {track['artist']}")
181
 
182
  except Exception as e:
183
  print(f"❌ Error searching YouTube: {e}")
src/mcp_servers/podcast_server.py CHANGED
@@ -17,6 +17,57 @@ class PodcastMCPServer:
17
  def __init__(self):
18
  self.name = "podcast_server"
19
  self.description = "Provides podcast recommendations from YouTube"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  def search_youtube_podcast(self, query: str, category: str = "technology", limit: int = 5) -> List[Dict[str, Any]]:
22
  """
@@ -58,18 +109,28 @@ class PodcastMCPServer:
58
  print("❌ No podcast entries found")
59
  return self._get_demo_podcasts(category, limit)
60
 
61
- # Filter and format results
62
  podcasts = []
 
 
63
  for entry in entries:
64
  if entry is None:
65
  continue
66
 
 
 
 
67
  video_id = entry.get('id', '')
68
  title = entry.get('title', 'Unknown Podcast')
69
  channel = entry.get('uploader', entry.get('channel', 'Unknown Host'))
70
  duration = entry.get('duration', 0)
71
 
72
  if video_id:
 
 
 
 
 
73
  podcasts.append({
74
  "title": title,
75
  "description": f"Podcast episode about {category}",
@@ -82,12 +143,11 @@ class PodcastMCPServer:
82
  "youtube_id": video_id,
83
  "url": f"https://www.youtube.com/watch?v={video_id}"
84
  })
 
85
 
86
  if podcasts:
87
- # Shuffle and return limited results
88
- random.shuffle(podcasts)
89
- print(f"✅ Found {len(podcasts)} podcasts on YouTube")
90
- return podcasts[:limit]
91
  else:
92
  return self._get_demo_podcasts(category, limit)
93
 
 
17
  def __init__(self):
18
  self.name = "podcast_server"
19
  self.description = "Provides podcast recommendations from YouTube"
20
+ self._embed_cache = {}
21
+
22
+ def check_video_embeddable(self, video_id: str) -> bool:
23
+ """Check if a YouTube video is embeddable"""
24
+ if video_id in self._embed_cache:
25
+ return self._embed_cache[video_id]
26
+
27
+ if not YT_DLP_AVAILABLE:
28
+ return True # Assume embeddable if we can't check
29
+
30
+ try:
31
+ ydl_opts = {
32
+ 'quiet': True,
33
+ 'no_warnings': True,
34
+ 'skip_download': True,
35
+ }
36
+
37
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
38
+ url = f"https://www.youtube.com/watch?v={video_id}"
39
+ info = ydl.extract_info(url, download=False)
40
+
41
+ if not info:
42
+ self._embed_cache[video_id] = False
43
+ return False
44
+
45
+ # Check availability
46
+ availability = info.get('availability')
47
+ if availability and availability != 'public':
48
+ print(f"⚠️ Podcast {video_id}: not public ({availability})")
49
+ self._embed_cache[video_id] = False
50
+ return False
51
+
52
+ # Check age restriction
53
+ if info.get('age_limit', 0) > 0:
54
+ print(f"⚠️ Podcast {video_id}: age restricted")
55
+ self._embed_cache[video_id] = False
56
+ return False
57
+
58
+ # Check embed playability
59
+ if info.get('playable_in_embed') is False:
60
+ print(f"⚠️ Podcast {video_id}: not playable in embed")
61
+ self._embed_cache[video_id] = False
62
+ return False
63
+
64
+ self._embed_cache[video_id] = True
65
+ return True
66
+
67
+ except Exception as e:
68
+ print(f"⚠️ Podcast {video_id}: check failed - {e}")
69
+ self._embed_cache[video_id] = False
70
+ return False
71
 
72
  def search_youtube_podcast(self, query: str, category: str = "technology", limit: int = 5) -> List[Dict[str, Any]]:
73
  """
 
109
  print("❌ No podcast entries found")
110
  return self._get_demo_podcasts(category, limit)
111
 
112
+ # Filter and format results, checking embeddability
113
  podcasts = []
114
+ random.shuffle(entries) # Shuffle first for variety
115
+
116
  for entry in entries:
117
  if entry is None:
118
  continue
119
 
120
+ if len(podcasts) >= limit:
121
+ break
122
+
123
  video_id = entry.get('id', '')
124
  title = entry.get('title', 'Unknown Podcast')
125
  channel = entry.get('uploader', entry.get('channel', 'Unknown Host'))
126
  duration = entry.get('duration', 0)
127
 
128
  if video_id:
129
+ # Check if video is embeddable
130
+ if not self.check_video_embeddable(video_id):
131
+ print(f" ✗ Skipping non-embeddable podcast: {title[:50]}")
132
+ continue
133
+
134
  podcasts.append({
135
  "title": title,
136
  "description": f"Podcast episode about {category}",
 
143
  "youtube_id": video_id,
144
  "url": f"https://www.youtube.com/watch?v={video_id}"
145
  })
146
+ print(f" ✓ Found embeddable podcast: {title[:50]}")
147
 
148
  if podcasts:
149
+ print(f"✅ Found {len(podcasts)} embeddable podcasts on YouTube")
150
+ return podcasts
 
 
151
  else:
152
  return self._get_demo_podcasts(category, limit)
153