# naomusic.py # -*- encoding: UTF-8 -*- import subprocess import os import json import re import time import threading class NaoMusicPlayer: def __init__(self, session, nao_ip, ssh_port=22): self.session = session self.player = session.service("ALAudioPlayer") self.nao_ip = nao_ip self.ssh_port = ssh_port self.music_dir = "/home/nao/music" self.local_path = "/tmp/song.mp3" self.remote_path = f"{self.music_dir}/song.mp3" try: subprocess.run(["sshpass", "-p", "nao", "ssh", "-p", str(self.ssh_port), f"nao@{nao_ip}", f"mkdir -p {self.music_dir}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except: pass self.current_file = None self.current_task_id = None self.is_playing = False self.current_title = "" self.is_loading = False self.load_error = None self.status = "" self.progress = 0.0 def search(self, query): try: cmd = ["yt-dlp", f"ytsearch5:{query}", "--flat-playlist", "--dump-json", "--no-warnings", "--quiet"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) entries = [] for line in result.stdout.strip().splitlines(): if not line.strip(): continue try: data = json.loads(line) except json.JSONDecodeError: continue vid = data.get("id") url = data.get("url") if not url or not url.startswith("http"): url = f"https://www.youtube.com/watch?v={vid}" if vid else None if url: entries.append({"url": url, "title": data.get("title", "Unknown")}) return entries except Exception as e: print(f"Search error: {e}") return [] _DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%') def play(self, url, title): self.stop() self.current_title = title self.is_loading = True self.load_error = None self.status = "Starting download..." self.progress = 0.0 try: if os.path.exists(self.local_path): os.remove(self.local_path) except: pass try: cmd = ["yt-dlp", url, "-x", "--audio-format", "mp3", "-o", self.local_path, "--no-playlist", "--no-warnings", "--newline"] print(f"\n▶ yt-dlp {url}") proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) for line in proc.stdout: line = line.rstrip() if not line: continue print(f" [yt-dlp] {line}") m = self._DL_PROGRESS_RE.search(line) if m: self.progress = float(m.group(1)) self.status = f"Downloading... {self.progress:.0f}%" elif "ExtractAudio" in line or "Extracting audio" in line: self.status = "Converting to mp3..." elif line.startswith("[download] Destination"): self.status = "Downloading..." proc.wait(timeout=120) if proc.returncode != 0 or not os.path.exists(self.local_path): print(f"Download failed (exit {proc.returncode})") self.load_error = "Download failed" return False size_mb = os.path.getsize(self.local_path) / (1024 * 1024) print(f"▶ scp {self.local_path} -> nao@{self.nao_ip}:{self.remote_path} ({size_mb:.1f} MB)") self.progress = 0.0 upload_start = time.time() stop_ticker = threading.Event() def ticker(): while not stop_ticker.is_set(): elapsed = time.time() - upload_start self.status = f"Uploading to NAO ({size_mb:.1f} MB)... {elapsed:.0f}s" stop_ticker.wait(1.0) t = threading.Thread(target=ticker, daemon=True) t.start() scp_result = subprocess.run( ["sshpass", "-p", "nao", "scp", "-P", str(self.ssh_port), self.local_path, f"nao@{self.nao_ip}:{self.remote_path}"], capture_output=True, text=True, timeout=60 ) stop_ticker.set() t.join(timeout=1.0) print(f" [scp] exit {scp_result.returncode}" + (f": {scp_result.stderr.strip()}" if scp_result.stderr.strip() else "")) if scp_result.returncode != 0: print(f"SCP failed: {scp_result.stderr.strip()}") self.load_error = "SCP failed" return False self.current_file = self.remote_path self.status = "Playing" self.progress = 100.0 self.current_task_id = self.player.playFile(self.remote_path) self.is_playing = True print(f"♪ Playing on NAO: {title} (task {self.current_task_id})") return True except Exception as e: print(f"Playback error: {e}") self.load_error = str(e) return False finally: self.is_loading = False def stop(self): try: self.player.stopAll() except Exception as e: print(f"Stop error: {e}") self.is_playing = False self.current_file = None self.current_task_id = None self.current_title = ""