feat: playlist import, show, and shuffle-on-load

Add !playlist import <name> <url> to resolve and save tracks from a URL
without queueing them. Add !playlist show <name> to display tracks in a
saved playlist via long_reply (auto-pastes on overflow). Add optional
'shuffle' keyword to !playlist load for randomized playback order.
This commit is contained in:
user
2026-02-22 20:31:54 +01:00
parent a76d46b1de
commit 40c6bf8c53

View File

@@ -1676,13 +1676,15 @@ async def _kept_repair(bot, message) -> None:
@command("playlist", help="Music: !playlist save|load|list|del <name>")
async def cmd_playlist(bot, message):
"""Save, load, list, or delete named playlists.
"""Save, load, list, delete, import, or show named playlists.
Usage:
!playlist save <name> Save current + queued tracks as a playlist
!playlist load <name> Append saved playlist to queue and start playback
!playlist list Show saved playlists with track counts
!playlist del <name> Delete a saved playlist
!playlist save <name> Save current + queued tracks as a playlist
!playlist load <name> [shuffle] Append saved playlist to queue
!playlist list Show saved playlists with track counts
!playlist del <name> Delete a saved playlist
!playlist import <name> <url> Import tracks from URL as a named playlist
!playlist show <name> Display tracks in a saved playlist
"""
if not _is_mumble(bot):
await bot.reply(message, "Mumble-only feature")
@@ -1691,7 +1693,7 @@ async def cmd_playlist(bot, message):
parts = message.text.split()
if len(parts) < 2:
await bot.reply(
message, "Usage: !playlist save|load|list|del <name>",
message, "Usage: !playlist save|load|list|del|import|show <name>",
)
return
@@ -1723,9 +1725,10 @@ async def cmd_playlist(bot, message):
elif sub == "load":
if len(parts) < 3:
await bot.reply(message, "Usage: !playlist load <name>")
await bot.reply(message, "Usage: !playlist load <name> [shuffle]")
return
name = parts[2].lower()
shuffle = len(parts) >= 4 and parts[3].lower() == "shuffle"
raw = bot.state.get("music", f"playlist:{name}")
if not raw:
await bot.reply(message, f"No playlist named '{name}'")
@@ -1746,9 +1749,12 @@ async def cmd_playlist(bot, message):
requester=e.get("requester", "?"),
))
added += 1
if shuffle and ps["queue"]:
random.shuffle(ps["queue"])
suffix = " (shuffled)" if shuffle else ""
await bot.reply(
message,
f"Loaded '{name}': {added} track{'s' if added != 1 else ''}",
f"Loaded '{name}': {added} track{'s' if added != 1 else ''}{suffix}",
)
if was_idle:
_ensure_loop(bot)
@@ -1791,9 +1797,58 @@ async def cmd_playlist(bot, message):
bot.state.delete("music", f"playlist:{name}")
await bot.reply(message, f"Deleted playlist '{name}'")
elif sub == "import":
if len(parts) < 4:
await bot.reply(message, "Usage: !playlist import <name> <url>")
return
name = parts[2].lower()
url = parts[3]
await bot.reply(message, f"Importing '{name}' from URL...")
loop = asyncio.get_running_loop()
try:
resolved = await loop.run_in_executor(None, _resolve_tracks, url)
except Exception:
await bot.reply(message, "Failed to resolve URL")
return
if not resolved:
await bot.reply(message, "No tracks found")
return
requester = message.nick or "?"
entries = [{"url": u, "title": t, "requester": requester}
for u, t in resolved]
bot.state.set("music", f"playlist:{name}", json.dumps(entries))
await bot.reply(
message,
f"Imported playlist '{name}' ({len(entries)} track"
f"{'s' if len(entries) != 1 else ''})",
)
elif sub == "show":
if len(parts) < 3:
await bot.reply(message, "Usage: !playlist show <name>")
return
name = parts[2].lower()
raw = bot.state.get("music", f"playlist:{name}")
if not raw:
await bot.reply(message, f"No playlist named '{name}'")
return
try:
entries = json.loads(raw)
except (json.JSONDecodeError, TypeError):
await bot.reply(message, f"Corrupt playlist '{name}'")
return
if not entries:
await bot.reply(message, f"Playlist '{name}' is empty")
return
lines = [f"Playlist '{name}' ({len(entries)} tracks):"]
for i, e in enumerate(entries, 1):
title = _truncate(e.get("title", e["url"]))
lines.append(f" {i:>2}. {title}")
await bot.long_reply(message, lines, label=name)
else:
await bot.reply(
message, "Usage: !playlist save|load|list|del <name>",
message, "Usage: !playlist save|load|list|del|import|show <name>",
)