From 40c6bf8c532c2c4e5e83ec0090123528757d5bbf Mon Sep 17 00:00:00 2001 From: user Date: Sun, 22 Feb 2026 20:31:54 +0100 Subject: [PATCH] feat: playlist import, show, and shuffle-on-load Add !playlist import to resolve and save tracks from a URL without queueing them. Add !playlist show 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. --- plugins/music.py | 73 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/plugins/music.py b/plugins/music.py index d3b4c55..66cede7 100644 --- a/plugins/music.py +++ b/plugins/music.py @@ -1676,13 +1676,15 @@ async def _kept_repair(bot, message) -> None: @command("playlist", help="Music: !playlist save|load|list|del ") 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 Save current + queued tracks as a playlist - !playlist load Append saved playlist to queue and start playback - !playlist list Show saved playlists with track counts - !playlist del Delete a saved playlist + !playlist save Save current + queued tracks as a playlist + !playlist load [shuffle] Append saved playlist to queue + !playlist list Show saved playlists with track counts + !playlist del Delete a saved playlist + !playlist import Import tracks from URL as a named playlist + !playlist show 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 ", + message, "Usage: !playlist save|load|list|del|import|show ", ) 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 ") + await bot.reply(message, "Usage: !playlist load [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 ") + 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 ") + 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 ", + message, "Usage: !playlist save|load|list|del|import|show ", )