151 lines
5.7 KiB
Python
151 lines
5.7 KiB
Python
# 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 = ""
|