Skip to content

Commit

Permalink
Improve playlists (#219)
Browse files Browse the repository at this point in the history
* Make playlist a group of commands

* Add playlist remove_song command

* Add playlist move_song command

* Display playlist length on invalid position

* Add playlist list command

* Add playlist show command

* Bump Pycord
  • Loading branch information
solaluset authored Dec 5, 2024
1 parent 7b7d467 commit 2922878
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 45 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ repos:
rev: 7.1.1
hooks:
- id: flake8
args: ["--ignore", "E203,W503"]

- repo: local
hooks:
Expand Down
8 changes: 8 additions & 0 deletions config/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@
"PLAYLIST_REMOVED": "Playlist removed.",
"HELP_ADD_TO_PLAYLIST_SHORT": "Add song to playlist",
"HELP_ADD_TO_PLAYLIST_LONG": "Adds the song or playlist to the saved playlist",
"HELP_REMOVE_FROM_PLAYLIST_SHORT": "Remove song from playlist",
"HELP_REMOVE_FROM_PLAYLIST_LONG": "Remove the song on specified position from playlist",
"HELP_MOVE_IN_PLAYLIST_SHORT": "Moves song",
"HELP_MOVE_IN_PLAYLIST_LONG": "Moves song in playlist from one position to another",
"HELP_LIST_PLAYLISTS_SHORT": "Lists playlists",
"HELP_LIST_PLAYLISTS_LONG": "Shows all playlists on this server",
"HELP_PLAYLIST_SHOW_SHORT": "Shows playlist",
"HELP_PLAYLIST_SHOW_LONG": "Shows all tracks in the playlist",
"PLAYLIST_UPDATED": "Playlist updated.",
"PLAYLISTS_ARE_DISABLED": "Playlists are disabled globally in this bot.",

Expand Down
4 changes: 2 additions & 2 deletions musicbot/audiocontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def next_song(self, error=None, *, forced=False):
async def play_song(self, song: Song):
"""Plays a song object"""

if not await loader.preload(song):
if not await loader.preload(song, self.bot):
self.next_song(forced=True)
return

Expand Down Expand Up @@ -395,7 +395,7 @@ async def _preload_queue(self):
for song in list(
islice(self.playlist.playque, 1, config.MAX_SONG_PRELOAD)
):
if not await loader.preload(song):
if not await loader.preload(song, self.bot):
try:
self.playlist.playque.remove(song)
rerun_needed = True
Expand Down
3 changes: 3 additions & 0 deletions musicbot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
GuildSettings,
run_migrations,
extract_legacy_settings,
migrate_old_playlists,
)
from musicbot.utils import CheckError

Expand Down Expand Up @@ -49,6 +50,8 @@ async def start(self, *args, **kwargs):
async with self.db_engine.connect() as connection:
await connection.run_sync(run_migrations)
await extract_legacy_settings(self)
await migrate_old_playlists(self)

return await super().start(*args, **kwargs)

async def close(self):
Expand Down
219 changes: 191 additions & 28 deletions musicbot/commands/music.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import json
from typing import Iterable, List, Union

from discord import Attachment, AutocompleteContext
from discord import Attachment, AutocompleteContext, Embed
from discord.ui import View
from discord.ext import commands, bridge
from discord.ext.bridge import BridgeOption
from discord.ext.pages import Paginator
from sqlalchemy import select, delete
from sqlalchemy.exc import IntegrityError

from config import config
from musicbot import linkutils, utils, loader
from musicbot.song import Song
from musicbot.bot import MusicBot, Context
from musicbot.utils import dj_check
from musicbot.utils import dj_check, chunks
from musicbot.audiocontroller import PLAYLIST, AudioController, MusicButton
from musicbot.loader import SongError, search_youtube
from musicbot.playlist import PlaylistError, LoopMode
from musicbot.settings import SavedPlaylist
from musicbot.linkutils import Origins, SiteTypes
from musicbot.linkutils import get_site_type, url_regex


class AudioContext(Context):
Expand Down Expand Up @@ -65,7 +66,7 @@ async def cog_before_invoke(self, ctx: AudioContext):
name="play",
description=config.HELP_YT_LONG,
help=config.HELP_YT_SHORT,
aliases=["p", "yt", "pl"],
aliases=["p", "yt"],
)
async def _play(
self, ctx: AudioContext, *, track: str = None, file: Attachment = None
Expand Down Expand Up @@ -126,7 +127,6 @@ async def _search(self, ctx: AudioContext, *, query: str):
songs = []
for data in results:
song = Song(
linkutils.Origins.Default,
linkutils.SiteTypes.YT_DLP,
webpage_url=data["url"],
)
Expand Down Expand Up @@ -186,7 +186,7 @@ async def _pause(self, ctx: AudioContext):
name="queue",
description=config.HELP_QUEUE_LONG,
help=config.HELP_QUEUE_SHORT,
aliases=["playlist", "q"],
aliases=["q"],
)
@active_only
async def _queue(self, ctx: AudioContext):
Expand Down Expand Up @@ -343,31 +343,40 @@ async def _playlist_autocomplete(
)
).scalars()

@bridge.bridge_command(
name="save_playlist",
aliases=["spl"],
@bridge.bridge_group(
name="playlist",
aliases=["pl"],
invoke_without_command=True,
)
async def _playlist(self, ctx: AudioContext):
await ctx.send("Use subcommands to manage playlists.")

@_playlist.command(
name="save",
aliases=["s"],
description=config.HELP_SAVE_PLAYLIST_LONG,
help=config.HELP_SAVE_PLAYLIST_SHORT,
)
@commands.check(dj_check)
async def _save_playlist(self, ctx: AudioContext, name: str):
async def _playlist_save(self, ctx: AudioContext, name: str):
if not config.ENABLE_PLAYLISTS:
await ctx.send(config.PLAYLISTS_ARE_DISABLED)
return

await ctx.defer()
urls = [
song.webpage_url for song in ctx.audiocontroller.playlist.playque
songs = [
{"url": song.webpage_url, "title": song.title}
for song in ctx.audiocontroller.playlist.playque
]
if not urls:
if not songs:
await ctx.send(config.QUEUE_EMPTY)
return
async with ctx.bot.DbSession() as session:
session.add(
SavedPlaylist(
guild_id=str(ctx.guild.id),
name=name,
songs_json=json.dumps(urls),
songs_json=json.dumps(songs),
)
)
try:
Expand All @@ -377,13 +386,13 @@ async def _save_playlist(self, ctx: AudioContext, name: str):
return
await ctx.send(config.PLAYLIST_SAVED_MESSAGE)

@bridge.bridge_command(
name="load_playlist",
aliases=["lpl"],
@_playlist.command(
name="load",
aliases=["l"],
description=config.HELP_LOAD_PLAYLIST_LONG,
help=config.HELP_LOAD_PLAYLIST_SHORT,
)
async def _load_playlist(
async def _playlist_load(
self,
ctx: AudioContext,
name: BridgeOption(str, autocomplete=_playlist_autocomplete),
Expand All @@ -400,24 +409,31 @@ async def _load_playlist(
if playlist is None:
await ctx.send(config.PLAYLIST_NOT_FOUND)
return
for url in json.loads(playlist.songs_json):
for song_data in json.loads(playlist.songs_json):
ctx.audiocontroller.playlist.add(
Song(Origins.Playlist, SiteTypes.YT_DLP, url)
Song(
get_site_type(song_data["url"]),
song_data["url"],
title=song_data["title"],
playlist=playlist,
)
)
if not ctx.audiocontroller.is_active():
await ctx.audiocontroller.play_song(
ctx.audiocontroller.playlist[0]
)
else:
ctx.audiocontroller.preload_queue()
await ctx.send(config.SONGINFO_PLAYLIST_QUEUED)

@bridge.bridge_command(
name="remove_playlist",
aliases=["rpl"],
@_playlist.command(
name="remove",
aliases=["r"],
description=config.HELP_REMOVE_PLAYLIST_LONG,
help=config.HELP_REMOVE_PLAYLIST_SHORT,
)
@commands.check(dj_check)
async def _remove_playlist(
async def _playlist_remove(
self,
ctx: AudioContext,
name: BridgeOption(str, autocomplete=_playlist_autocomplete),
Expand All @@ -435,14 +451,79 @@ async def _remove_playlist(
return
await ctx.send(config.PLAYLIST_REMOVED)

@bridge.bridge_command(
name="add_to_playlist",
aliases=["apl"],
@_playlist.command(
name="list",
aliases=["li"],
description=config.HELP_LIST_PLAYLISTS_LONG,
help=config.HELP_LIST_PLAYLISTS_SHORT,
)
async def _playlist_list(self, ctx: AudioContext):
async with ctx.bot.DbSession() as session:
playlists = (
(
await session.execute(
select(SavedPlaylist.name).where(
SavedPlaylist.guild_id == str(ctx.guild.id)
)
)
)
.scalars()
.all()
)
if not playlists:
await ctx.send("No playlists.")
return

playlist_names = "\n".join(f"- {name}" for name in playlists)
await ctx.send(f"**Playlists:**\n{playlist_names}")

@_playlist.command(
name="show",
aliases=["sw"],
description=config.HELP_PLAYLIST_SHOW_LONG,
help=config.HELP_PLAYLIST_SHOW_SHORT,
)
@commands.check(dj_check)
async def _playlist_show(
self,
ctx: AudioContext,
playlist: BridgeOption(str, autocomplete=_playlist_autocomplete),
):
await ctx.defer()

async with ctx.bot.DbSession() as session:
playlist = (
await session.execute(
select(SavedPlaylist)
.where(SavedPlaylist.guild_id == str(ctx.guild.id))
.where(SavedPlaylist.name == playlist)
)
).scalar_one_or_none()
if playlist is None:
await ctx.send(config.PLAYLIST_NOT_FOUND)
return
pages = []
i = 1
for part in chunks(json.loads(playlist.songs_json), 25):
embed = Embed(title=playlist.name)
for song in part:
url = song["url"]
title = song["title"] or url_regex.fullmatch(url).group("bare")
embed.add_field(
name=str(i), value=f"[{title}]({url})", inline=False
)
i += 1
pages.append(embed)
await Paginator(pages).send(ctx)

@_playlist.command(
name="add_song",
aliases=["as"],
description=config.HELP_ADD_TO_PLAYLIST_LONG,
help=config.HELP_ADD_TO_PLAYLIST_SHORT,
)
@commands.check(dj_check)
async def _add_to_playlist(
async def _playlist_add_song(
self,
ctx: AudioContext,
playlist: BridgeOption(str, autocomplete=_playlist_autocomplete),
Expand Down Expand Up @@ -475,6 +556,88 @@ async def _add_to_playlist(
await session.commit()
await ctx.send(config.PLAYLIST_UPDATED)

@_playlist.command(
name="remove_song",
aliases=["rs"],
description=config.HELP_REMOVE_FROM_PLAYLIST_LONG,
help=config.HELP_REMOVE_FROM_PLAYLIST_SHORT,
)
@commands.check(dj_check)
async def _playlist_remove_song(
self,
ctx: AudioContext,
playlist: BridgeOption(str, autocomplete=_playlist_autocomplete),
position: int,
):
await ctx.defer()

async with ctx.bot.DbSession() as session:
playlist = (
await session.execute(
select(SavedPlaylist)
.where(SavedPlaylist.guild_id == str(ctx.guild.id))
.where(SavedPlaylist.name == playlist)
)
).scalar_one_or_none()
if playlist is None:
await ctx.send(config.PLAYLIST_NOT_FOUND)
return
songs = json.loads(playlist.songs_json)
if position <= 0 or position > len(songs):
await ctx.send(
f"Invalid position. Playlist has {len(songs)} songs."
)
return
if len(songs) == 1:
await ctx.send("Can't remove the only song from playlist.")
return
del songs[position - 1]
playlist.songs_json = json.dumps(songs)
await session.commit()
await ctx.send(config.PLAYLIST_UPDATED)

@_playlist.command(
name="move_song",
aliases=["ms"],
description=config.HELP_MOVE_IN_PLAYLIST_LONG,
help=config.HELP_MOVE_IN_PLAYLIST_SHORT,
)
@commands.check(dj_check)
async def _playlist_move_song(
self,
ctx: AudioContext,
playlist: BridgeOption(str, autocomplete=_playlist_autocomplete),
source_position: int,
destination_position: int,
):
await ctx.defer()

async with ctx.bot.DbSession() as session:
playlist = (
await session.execute(
select(SavedPlaylist)
.where(SavedPlaylist.guild_id == str(ctx.guild.id))
.where(SavedPlaylist.name == playlist)
)
).scalar_one_or_none()
if playlist is None:
await ctx.send(config.PLAYLIST_NOT_FOUND)
return
songs = json.loads(playlist.songs_json)
if min(source_position, destination_position) <= 0 or max(
source_position, destination_position
) > len(songs):
await ctx.send(
f"Invalid position. Playlist has {len(songs)} songs."
)
return
songs.insert(
destination_position - 1, songs.pop(source_position - 1)
)
playlist.songs_json = json.dumps(songs)
await session.commit()
await ctx.send(config.PLAYLIST_UPDATED)


def setup(bot: MusicBot):
bot.add_cog(Music(bot))
Loading

0 comments on commit 2922878

Please sign in to comment.