Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Music 2.0 #36

Merged
merged 9 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 67 additions & 116 deletions nameless/cogs/MusicCog.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import datetime
import logging
import random
from typing import cast

import discord
Expand All @@ -16,13 +15,14 @@

from nameless import Nameless
from nameless.cogs.checks.MusicCogCheck import MusicCogCheck
from nameless.customs import NamelessPlayer, QueueAction
from nameless.customs import NamelessPlayer
from nameless.customs.ui_kit import NamelessTrackDropdown, NamelessVoteMenu
from nameless.database import CRUD
from NamelessConfig import NamelessConfig

__all__ = ["MusicCog"]

# source -> type
SOURCE_MAPPING = {
"youtube": wavelink.TrackSource.YouTube,
"soundcloud": wavelink.TrackSource.SoundCloud,
Expand Down Expand Up @@ -75,25 +75,25 @@ async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload
return

chn = player.guild.get_channel(player.trigger_channel_id)
can_send = (
chn is not None
and player.play_now_allowed
and player.should_send_play_now
and player.queue.mode is not QueueMode.loop
)

if not isinstance(chn, discord.abc.Messageable):
return

can_send = not player.current.is_stream and player.play_now_allowed and player.queue.mode is not QueueMode.loop

if not can_send:
return
else:
dbg = CRUD.get_or_create_guild_record(player.guild)

dbg = CRUD.get_or_create_guild_record(player.guild)
if chn is not None and player.play_now_allowed and player.should_send_play_now:
embed = self.generate_embed_from_track(
player,
track,
self.bot.user,
dbg,
original is not None and original.recommended,
)

await chn.send(embed=embed)

@commands.Cog.listener()
Expand Down Expand Up @@ -252,7 +252,7 @@ async def connect(self, interaction: discord.Interaction):
await interaction.response.defer()

try:
await interaction.user.voice.channel.connect(cls=wavelink.Player, self_deaf=True)
await cast(discord.Member, interaction.user).voice.channel.connect(cls=wavelink.Player, self_deaf=True)
await interaction.followup.send("Connected to your voice channel")

player = cast(NamelessPlayer, interaction.guild.voice_client) # type: ignore
Expand Down Expand Up @@ -312,89 +312,6 @@ async def pick_track_from_results(
pick_list: list[wavelink.Playable] = [tracks[int(val)] for val in vals]
return pick_list

async def _play(
self,
interaction: discord.Interaction,
query: str,
source: str = "youtube",
action: QueueAction = QueueAction.ADD,
reverse: bool = False,
shuffle: bool = False,
):
"""
Add or insert a track or playlist in the player queue.

Parameters:
----------
interaction (discord.Interaction): The interaction object representing the user's interaction with the bot.
query (str): The search query for the track or playlist.
source (str, optional): The source of the track or playlist (default: "youtube").
action (str, optional): The action to perform on the track or playlist (default: "add").
reverse (bool, optional): Whether to reverse the order of the tracks (default: False).
shuffle (bool, optional): Whether to shuffle the order of the tracks (default: False).

Raises:
----------
wavelink.LavalinkLoadException: If there is an error loading the track or playlist.

"""
await interaction.response.defer()

player: NamelessPlayer = cast(NamelessPlayer, interaction.guild.voice_client) # type: ignore
should_play = not player.playing and not bool(player.queue) and player.auto_play_queue
msg: str = ""

async def add_to_queue(tracks: list[wavelink.Playable] | wavelink.Playlist) -> int:
if reverse:
if isinstance(tracks, wavelink.Playlist):
tracks = list(reversed(tracks))
else:
tracks.reverse()

if shuffle:
random.shuffle(tracks if isinstance(tracks, list) else tracks.tracks)

if action == QueueAction.ADD:
return await player.queue.put_wait(tracks)
elif action == QueueAction.INSERT:
return await player.queue.insert_wait(tracks)
return 0

try:
tracks: wavelink.Search = await wavelink.Playable.search(query, source=SOURCE_MAPPING[source])
except wavelink.LavalinkLoadException as err:
logging.error(err)
await interaction.followup.send("Lavalink error occurred. Please contact the bot owner.")
return

if not tracks:
await interaction.followup.send("No results found")
return

if isinstance(tracks, wavelink.Playlist):
added = await add_to_queue(tracks)
soon_added = tracks
msg = f"Added the playlist **`{tracks.name}`** ({added} songs) to the queue."
else:
soon_added = await self.pick_track_from_results(interaction, tracks)

if not soon_added:
return

added = await add_to_queue(soon_added)
msg = f"{action.name.title()}ed {added} {'songs' if added > 1 else 'song'} to the queue"

if soon_added:
embeds = self.generate_embeds_from_tracks(soon_added, embed_title=msg)
self.bot.loop.create_task(self.show_paginated_tracks(interaction, embeds))

if player.current and player.current.is_stream:
should_play = True
await player.stop(force=True)

if should_play:
await player.play(player.queue.get(), add_history=False)

@app_commands.command()
@app_commands.guild_only()
@app_commands.check(MusicCogCheck.user_and_bot_in_voice)
Expand Down Expand Up @@ -479,7 +396,7 @@ async def skip(self, interaction: discord.Interaction):

if (
# The invoker has the MANAGE_GUILD
interaction.user.guild_permissions.manage_guild
cast(discord.Member, interaction.user).guild_permissions.manage_guild
or
# Only you & the bot
len(player.client.users) == 2
Expand Down Expand Up @@ -563,38 +480,70 @@ async def start(self, interaction: discord.Interaction):

await interaction.followup.send("Started playing the queue")

@queue.command()
@app_commands.guild_only()
@app_commands.describe(query="Search query", source="Source to search")
@app_commands.choices(source=[Choice(name=k, value=k) for k in SOURCE_MAPPING])
@app_commands.check(MusicCogCheck.user_and_bot_in_voice)
async def add(self, interaction: discord.Interaction, query: str, source: str = "youtube"):
"""Alias for `play` command."""
await self._play(interaction, query, source)

@queue.command()
@app_commands.guild_only()
@app_commands.describe(
url="Playlist URL",
position="Position to add the playlist, '-1' means at the end of queue",
reverse="Process pending playlist in reversed order",
shuffle="Process pending playlist in shuffled order",
source="Playlist URL or query search.",
position="Position to add the playlist, '0' means at the end of queue.",
origin="Where to search for your source, defaults to 'YouTube' origin.",
reverse="Whether to reverse the input track list before adding to queue. Has higher precedence.",
shuffle="Whether to shuffle the input track list before adding to queue. Has lower precedence.",
)
@app_commands.choices(origin=[Choice(name=k, value=k) for k in SOURCE_MAPPING])
@app_commands.check(MusicCogCheck.user_and_bot_in_voice)
async def add_playlist(
async def add(
self,
interaction: discord.Interaction,
url: str,
position: app_commands.Range[int, -1] = -1,
source: str,
position: app_commands.Range[int, 0] = 0,
origin: str = "youtube",
reverse: bool = False,
shuffle: bool = False,
):
"""Add playlist to the queue."""
await interaction.response.defer()

player: NamelessPlayer = cast(NamelessPlayer, interaction.guild.voice_client) # type: ignore
player: NamelessPlayer = cast(NamelessPlayer, interaction.guild.voice_client)
msg: str = ""

tracks: wavelink.Search = await wavelink.Playable.search(source, source=SOURCE_MAPPING[origin])

if not tracks:
await interaction.followup.send("No results found.")
return

if isinstance(tracks, wavelink.Playlist):
soon_added = tracks.tracks
msg = f"Added the playlist **`{tracks.name}`** ({tracks.tracks.count} songs) to the queue."
else:
soon_added = await self.pick_track_from_results(interaction, tracks)

await self._play(interaction, url, reverse=reverse, shuffle=shuffle)
if not soon_added:
await interaction.followup.send("Nothing will be added.")
return

msg = f"Added {soon_added.count} track(s) to the queue."

if reverse:
player.queue._items = list(reversed(player.queue._items))

if shuffle:
player.queue.shuffle()

if soon_added:
embeds = self.generate_embeds_from_tracks(soon_added, embed_title=msg)
self.bot.loop.create_task(self.show_paginated_tracks(interaction, embeds))

position_to_add = position if position >= 0 else -1

if position_to_add == 0:
await player.queue.put_wait(soon_added)
else:
position -= 1
player.queue._items = player.queue._items[:position] + soon_added + player.queue._items[position:]

if not player.current:
await player.play(player.queue.get())

@queue.command()
@app_commands.guild_only()
Expand Down Expand Up @@ -642,7 +591,9 @@ async def repopulate_autoqueue(self, interaction: discord.Interaction):
await interaction.followup.send("Seems like autoplay is disabled.")
return

await player.repopulate_auto_queue()
player.auto_queue.clear()
await player._do_recommendation()

await interaction.followup.send("Repopulated autoplay queue!")

@queue.command()
Expand Down Expand Up @@ -744,7 +695,7 @@ async def clear(self, interaction: discord.Interaction):

if (
# The invoker has the MANAGE_GUILD
interaction.user.guild_permissions.manage_guild
cast(discord.Member, interaction.user).guild_permissions.manage_guild
or
# Only you & the bot
len(player.client.users) == 2
Expand All @@ -763,7 +714,7 @@ async def clear(self, interaction: discord.Interaction):
@app_commands.guild_only()
@app_commands.check(MusicCogCheck.user_and_bot_in_voice)
@app_commands.describe(channel="The target channel for 'Now playing' message delivery.")
async def set_feed_channel(self, interaction: discord.Interaction, channel: discord.abc.Messageable):
async def set_feed_channel(self, interaction: discord.Interaction, channel: discord.abc.GuildChannel):
"""Change where the now-playing messages are sent."""
await interaction.response.defer()

Expand Down
100 changes: 5 additions & 95 deletions nameless/customs/NamelessPlayer.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,10 @@
import logging

import discord
import wavelink
from wavelink import AutoPlayMode

from nameless.customs import NamelessQueue

__all__ = ["NamelessPlayer"]


class NamelessPlayer(wavelink.Player):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.autoplay = wavelink.AutoPlayMode.partial
self.queue: NamelessQueue = NamelessQueue()

self._cog = None # maybe useful for later
self._should_send_play_now = True
self._play_now_allowed = True
self._trigger_channel_id = self.channel.id
self._auto_play_queue = True

@property
def auto_play_queue(self) -> bool:
return self._auto_play_queue

@auto_play_queue.setter
def auto_play_queue(self, value: bool):
self._auto_play_queue = value

@property
def should_send_play_now(self) -> bool:
return self._should_send_play_now

@should_send_play_now.setter
def should_send_play_now(self, value: bool):
self._should_send_play_now = value

@property
def play_now_allowed(self) -> bool:
"""
Check if 'Now playing' message should be sent.
"""
return self._play_now_allowed

@play_now_allowed.setter
def play_now_allowed(self, value: bool):
self._play_now_allowed = value

@property
def trigger_channel_id(self) -> int:
"""
Store channel Id that triggered this player.
"""
return self._trigger_channel_id

@trigger_channel_id.setter
def trigger_channel_id(self, value: int):
self._trigger_channel_id = value

async def repopulate_auto_queue(self):
"""
Repopulate autoplay queue. This snippet is copy from wavelink `_auto_play_event`.
"""
if self.autoplay is AutoPlayMode.enabled:
async with self._auto_lock:
self.auto_queue.clear()
await self._do_recommendation()

async def set_autoplay_mode(self, value: AutoPlayMode | int):
if isinstance(value, int):
try:
value = AutoPlayMode(value)
except ValueError:
logging.error(
"set_autoplay_mode received an invalid value. Want 'wavelink.AutoPlayMode' but received %s",
value.__class__.__name__,
)
return

self.autoplay = value
await self.repopulate_auto_queue()

async def toggle_autoplay(self) -> bool:
"""
Toggle autoplay like the one on Youtube, also repopulates autoplay queue base on new value.

Returns
-------
:class:`bool`
True if autoplay is enabled, False if disabled.
"""
if self.autoplay is AutoPlayMode.enabled:
self.autoplay = AutoPlayMode.partial
return False
def __init__(self, client: discord.Client, channel: discord.abc.Connectable, **kwargs):
super().__init__(client, channel, **kwargs)

self.autoplay = AutoPlayMode.enabled
await self.repopulate_auto_queue()
return True
self.trigger_channel_id: int = 0
self.play_now_allowed: int = 0
Loading
Loading