feat: add YouTube search to !play and fix NA URL fallback
Non-URL input (e.g. !play classical music) searches YouTube for 10 results and picks one randomly. Also fixes --flat-playlist returning "NA" as the URL for single videos by falling back to the original input URL.
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -51,6 +52,11 @@ def _truncate(text: str, max_len: int = _MAX_TITLE_LEN) -> str:
|
||||
return text[: max_len - 3].rstrip() + "..."
|
||||
|
||||
|
||||
def _is_url(text: str) -> bool:
|
||||
"""Check if text looks like a URL rather than a search query."""
|
||||
return text.startswith(("http://", "https://", "ytsearch:"))
|
||||
|
||||
|
||||
def _resolve_tracks(url: str, max_tracks: int = _MAX_QUEUE) -> list[tuple[str, str]]:
|
||||
"""Resolve URL into (url, title) pairs via yt-dlp. Blocking, run in executor.
|
||||
|
||||
@@ -73,8 +79,10 @@ def _resolve_tracks(url: str, max_tracks: int = _MAX_QUEUE) -> list[tuple[str, s
|
||||
for i in range(0, len(lines) - 1, 2):
|
||||
track_url = lines[i].strip()
|
||||
track_title = lines[i + 1].strip()
|
||||
if track_url:
|
||||
tracks.append((track_url, track_title or track_url))
|
||||
# --flat-playlist prints "NA" for single videos (no extraction)
|
||||
if not track_url or track_url == "NA":
|
||||
track_url = url
|
||||
tracks.append((track_url, track_title or track_url))
|
||||
return tracks if tracks else [(url, url)]
|
||||
except Exception:
|
||||
return [(url, url)]
|
||||
@@ -126,12 +134,13 @@ def _ensure_loop(bot) -> None:
|
||||
# -- Commands ----------------------------------------------------------------
|
||||
|
||||
|
||||
@command("play", help="Music: !play <url|playlist>")
|
||||
@command("play", help="Music: !play <url|query>")
|
||||
async def cmd_play(bot, message):
|
||||
"""Play a URL or add to queue if already playing.
|
||||
|
||||
Usage:
|
||||
!play <url> Play audio from URL (YouTube, SoundCloud, etc.)
|
||||
!play <url> Play audio from URL (YouTube, SoundCloud, etc.)
|
||||
!play <query> Search YouTube and play the first result
|
||||
|
||||
Playlists are expanded into individual tracks. If the queue is nearly
|
||||
full, only as many tracks as will fit are enqueued.
|
||||
@@ -142,10 +151,13 @@ async def cmd_play(bot, message):
|
||||
|
||||
parts = message.text.split(None, 1)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !play <url>")
|
||||
await bot.reply(message, "Usage: !play <url|query>")
|
||||
return
|
||||
|
||||
url = parts[1].strip()
|
||||
is_search = not _is_url(url)
|
||||
if is_search:
|
||||
url = f"ytsearch10:{url}"
|
||||
ps = _ps(bot)
|
||||
|
||||
if len(ps["queue"]) >= _MAX_QUEUE:
|
||||
@@ -156,6 +168,10 @@ async def cmd_play(bot, message):
|
||||
loop = asyncio.get_running_loop()
|
||||
resolved = await loop.run_in_executor(None, _resolve_tracks, url, remaining)
|
||||
|
||||
# Search: pick one random result instead of enqueuing all
|
||||
if is_search and len(resolved) > 1:
|
||||
resolved = [random.choice(resolved)]
|
||||
|
||||
was_idle = ps["current"] is None
|
||||
requester = message.nick or "?"
|
||||
added = 0
|
||||
|
||||
Reference in New Issue
Block a user