feat: expand YouTube playlists into individual queue tracks
_resolve_title replaced with _resolve_tracks using --flat-playlist to enumerate playlist entries. cmd_play enqueues each track individually, with truncation when the queue is nearly full. Single-video behavior unchanged.
This commit is contained in:
@@ -51,17 +51,33 @@ def _truncate(text: str, max_len: int = _MAX_TITLE_LEN) -> str:
|
||||
return text[: max_len - 3].rstrip() + "..."
|
||||
|
||||
|
||||
def _resolve_title(url: str) -> str:
|
||||
"""Resolve track title via yt-dlp. Blocking, run in executor."""
|
||||
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.
|
||||
|
||||
Handles both single videos and playlists. For playlists, returns up to
|
||||
``max_tracks`` individual entries. Falls back to ``[(url, url)]`` on error.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["yt-dlp", "--get-title", "--no-warnings", url],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
[
|
||||
"yt-dlp", "--flat-playlist", "--print", "url",
|
||||
"--print", "title", "--no-warnings",
|
||||
f"--playlist-end={max_tracks}", url,
|
||||
],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
title = result.stdout.strip()
|
||||
return title if title else url
|
||||
lines = result.stdout.strip().splitlines()
|
||||
if len(lines) < 2:
|
||||
return [(url, url)]
|
||||
tracks = []
|
||||
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))
|
||||
return tracks if tracks else [(url, url)]
|
||||
except Exception:
|
||||
return url
|
||||
return [(url, url)]
|
||||
|
||||
|
||||
# -- Play loop ---------------------------------------------------------------
|
||||
@@ -110,12 +126,15 @@ def _ensure_loop(bot) -> None:
|
||||
# -- Commands ----------------------------------------------------------------
|
||||
|
||||
|
||||
@command("play", help="Music: !play <url>")
|
||||
@command("play", help="Music: !play <url|playlist>")
|
||||
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.)
|
||||
|
||||
Playlists are expanded into individual tracks. If the queue is nearly
|
||||
full, only as many tracks as will fit are enqueued.
|
||||
"""
|
||||
if not _is_mumble(bot):
|
||||
await bot.reply(message, "Music playback is Mumble-only")
|
||||
@@ -133,20 +152,36 @@ async def cmd_play(bot, message):
|
||||
await bot.reply(message, f"Queue full ({_MAX_QUEUE} tracks)")
|
||||
return
|
||||
|
||||
remaining = _MAX_QUEUE - len(ps["queue"])
|
||||
loop = asyncio.get_running_loop()
|
||||
title = await loop.run_in_executor(None, _resolve_title, url)
|
||||
track = _Track(url=url, title=title, requester=message.nick or "?")
|
||||
resolved = await loop.run_in_executor(None, _resolve_tracks, url, remaining)
|
||||
|
||||
ps["queue"].append(track)
|
||||
was_idle = ps["current"] is None
|
||||
requester = message.nick or "?"
|
||||
added = 0
|
||||
for track_url, track_title in resolved[:remaining]:
|
||||
ps["queue"].append(_Track(url=track_url, title=track_title,
|
||||
requester=requester))
|
||||
added += 1
|
||||
|
||||
if ps["current"] is not None:
|
||||
pos = len(ps["queue"])
|
||||
total_resolved = len(resolved)
|
||||
|
||||
if added == 1:
|
||||
title = _truncate(resolved[0][1])
|
||||
if was_idle:
|
||||
await bot.reply(message, f"Playing: {title}")
|
||||
else:
|
||||
pos = len(ps["queue"])
|
||||
await bot.reply(message, f"Queued #{pos}: {title}")
|
||||
elif added < total_resolved:
|
||||
await bot.reply(
|
||||
message,
|
||||
f"Queued #{pos}: {_truncate(title)}",
|
||||
f"Queued {added} of {total_resolved} tracks (queue full)",
|
||||
)
|
||||
else:
|
||||
await bot.reply(message, f"Playing: {_truncate(title)}")
|
||||
await bot.reply(message, f"Queued {added} tracks")
|
||||
|
||||
if was_idle:
|
||||
_ensure_loop(bot)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user