From 4afd066ec80c6bf55dbcc1519821037311ad3a2f Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 15 Mar 2024 18:00:27 +0700 Subject: [PATCH 1/8] Nuke `NamelessQueue` custom class. --- nameless/cogs/MusicCog.py | 2 +- nameless/customs/NamelessQueue.py | 65 ------------------------------- nameless/customs/__init__.py | 1 - 3 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 nameless/customs/NamelessQueue.py diff --git a/nameless/cogs/MusicCog.py b/nameless/cogs/MusicCog.py index 7f9e918..58aed5c 100644 --- a/nameless/cogs/MusicCog.py +++ b/nameless/cogs/MusicCog.py @@ -16,7 +16,7 @@ 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 diff --git a/nameless/customs/NamelessQueue.py b/nameless/customs/NamelessQueue.py deleted file mode 100644 index 3d6130e..0000000 --- a/nameless/customs/NamelessQueue.py +++ /dev/null @@ -1,65 +0,0 @@ -import asyncio -from enum import Enum - -from wavelink import Playable, Playlist, Queue - -__all__ = ["QueueAction", "NamelessQueue"] - - -class QueueAction(Enum): - ADD = 0 - INSERT = 1 - - -class NamelessQueue(Queue): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def _insert(self, item: Playable) -> None: - self._check_compatability(item) - self._queue.insert(0, item) - - def insert(self, item: Playable | Playlist, /, *, atomic: bool = True) -> int: - added: int = 0 - - if isinstance(item, Playlist): - if atomic: - self._check_atomic(item) - - for track in item: - try: - self._insert(track) - added += 1 - except TypeError: - pass - - else: - self._insert(item) - added += 1 - - return added - - async def insert_wait(self, item: list[Playable] | Playable | Playlist, /, *, atomic: bool = True) -> int: - added: int = 0 - - async with self._lock: - if isinstance(item, list | Playlist): - if atomic: - super()._check_atomic(item) - - for track in item: - try: - self._insert(track) - added += 1 - except TypeError: - pass - - await asyncio.sleep(0) - - else: - self._insert(item) - added += 1 - await asyncio.sleep(0) - - self._wakeup_next() - return added diff --git a/nameless/customs/__init__.py b/nameless/customs/__init__.py index 993ddf7..ec6fd28 100644 --- a/nameless/customs/__init__.py +++ b/nameless/customs/__init__.py @@ -1,5 +1,4 @@ from .NamelessPlayer import * -from .NamelessQueue import * from .NamelessSharedVariables import NamelessSharedVariables from .NamelessSingleton import * From 619a474da6eada77d7b7f503317de2a4c85f33dc Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 15 Mar 2024 18:02:32 +0700 Subject: [PATCH 2/8] Huh? --- nameless/customs/NamelessPlayer.py | 100 ++--------------------------- 1 file changed, 5 insertions(+), 95 deletions(-) diff --git a/nameless/customs/NamelessPlayer.py b/nameless/customs/NamelessPlayer.py index dddafd1..413bf35 100644 --- a/nameless/customs/NamelessPlayer.py +++ b/nameless/customs/NamelessPlayer.py @@ -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 From 023c6f353bad579255825882982a07f2dc0d7a8e Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 16 Mar 2024 09:39:41 +0700 Subject: [PATCH 3/8] WYSI. --- nameless/cogs/MusicCog.py | 178 +++++++++++++++----------------------- 1 file changed, 68 insertions(+), 110 deletions(-) diff --git a/nameless/cogs/MusicCog.py b/nameless/cogs/MusicCog.py index 58aed5c..2decd2a 100644 --- a/nameless/cogs/MusicCog.py +++ b/nameless/cogs/MusicCog.py @@ -23,6 +23,7 @@ __all__ = ["MusicCog"] +# source -> type SOURCE_MAPPING = { "youtube": wavelink.TrackSource.YouTube, "soundcloud": wavelink.TrackSource.SoundCloud, @@ -74,19 +75,23 @@ async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload logging.warning("Player is not connected. Or we have been banned from the guild!") return + chn = player.guild.get_channel(player.trigger_channel_id) + + if not isinstance(chn, discord.abc.Messageable): + return + can_send = ( - chn is not None + not player.current.is_stream and player.play_now_allowed - and player.should_send_play_now 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, @@ -94,6 +99,7 @@ async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload dbg, original is not None and original.recommended, ) + await chn.send(embed=embed) @commands.Cog.listener() @@ -252,7 +258,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 @@ -312,89 +318,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) @@ -479,7 +402,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 @@ -563,38 +486,71 @@ 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() @@ -642,7 +598,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() @@ -744,7 +702,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 @@ -763,7 +721,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() From d142da94650190010ecdd1cd73fdbfdb9b8becee Mon Sep 17 00:00:00 2001 From: swyrin Date: Sat, 16 Mar 2024 02:55:00 +0000 Subject: [PATCH 4/8] [ci skip] Automated code format commit. --- nameless/cogs/MusicCog.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/nameless/cogs/MusicCog.py b/nameless/cogs/MusicCog.py index 2decd2a..1534d10 100644 --- a/nameless/cogs/MusicCog.py +++ b/nameless/cogs/MusicCog.py @@ -1,7 +1,6 @@ import asyncio import datetime import logging -import random from typing import cast import discord @@ -75,17 +74,12 @@ async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload logging.warning("Player is not connected. Or we have been banned from the guild!") return - chn = player.guild.get_channel(player.trigger_channel_id) 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 - ) + 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 @@ -493,7 +487,7 @@ async def start(self, interaction: discord.Interaction): 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." + 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) @@ -505,7 +499,6 @@ async def add( origin: str = "youtube", reverse: bool = False, shuffle: bool = False, - ): """Add playlist to the queue.""" await interaction.response.defer() From 7190a1d2db6a6d052884a9a69a7926db01ffe4e1 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 16 Mar 2024 09:43:38 +0700 Subject: [PATCH 5/8] Update CI run conditions. --- .github/workflows/nameless_release_experimental.yml | 3 +-- .github/workflows/nameless_validate.yml | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nameless_release_experimental.yml b/.github/workflows/nameless_release_experimental.yml index 44f916e..fe65db2 100644 --- a/.github/workflows/nameless_release_experimental.yml +++ b/.github/workflows/nameless_release_experimental.yml @@ -3,8 +3,6 @@ name: Create experimental release on: push: branches: - - 'feat/**' - - 'fix/**' - 'main' permissions: @@ -69,6 +67,7 @@ jobs: release: if: ${{ github.repository == 'team-nameless/nameless-discord-bot' }} runs-on: ubuntu-latest + needs: [ "python" ] steps: - name: Create cutting-edge release uses: "marvinpinto/action-automatic-releases@latest" diff --git a/.github/workflows/nameless_validate.yml b/.github/workflows/nameless_validate.yml index ee51745..2f9b909 100644 --- a/.github/workflows/nameless_validate.yml +++ b/.github/workflows/nameless_validate.yml @@ -1,10 +1,12 @@ name: Code validation on: - pull_request: + push: branches: - 'feat/**' - 'fix/**' + pull_request: + branches: - 'main' jobs: From 0f5c41586e53a533dfad308df44453cfb4d19827 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 16 Mar 2024 09:45:55 +0700 Subject: [PATCH 6/8] Update PyWrong inspection rules. --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 220fa73..3340046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,10 @@ exclude = [ "**/__pycache__", ] -reportWildcardImportFromLibrary = "none" +reportWildcardImportFromLibrary = "warning" reportUnnecessaryTypeIgnoreComment = "error" -reportUnusedImport = "warning" reportOptionalMemberAccess = "warning" reportOptionalSubscript = "warning" -reportAttributeAccessIssue = "warning" useLibraryCodeForTypes = true pythonVersion = "3.11" typeCheckingMode = "basic" From d7e12aaf09767c157dd3001f318577c43d148e5c Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 16 Mar 2024 09:48:52 +0700 Subject: [PATCH 7/8] Make experimental always be ran no matter what. --- .github/workflows/nameless_release_experimental.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nameless_release_experimental.yml b/.github/workflows/nameless_release_experimental.yml index fe65db2..8e3cd39 100644 --- a/.github/workflows/nameless_release_experimental.yml +++ b/.github/workflows/nameless_release_experimental.yml @@ -65,7 +65,7 @@ jobs: commit_message: "[ci skip] Automated code format commit." release: - if: ${{ github.repository == 'team-nameless/nameless-discord-bot' }} + if: ${{ github.repository == 'team-nameless/nameless-discord-bot' && always() }} runs-on: ubuntu-latest needs: [ "python" ] steps: From ca008c73f31fae329eae8e3647f20312f8b50369 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 16 Mar 2024 09:50:52 +0700 Subject: [PATCH 8/8] I forgor `always()` always return `true`. --- .github/workflows/nameless_release_experimental.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nameless_release_experimental.yml b/.github/workflows/nameless_release_experimental.yml index 8e3cd39..b557f84 100644 --- a/.github/workflows/nameless_release_experimental.yml +++ b/.github/workflows/nameless_release_experimental.yml @@ -65,7 +65,7 @@ jobs: commit_message: "[ci skip] Automated code format commit." release: - if: ${{ github.repository == 'team-nameless/nameless-discord-bot' && always() }} + if: ${{ github.repository == 'team-nameless/nameless-discord-bot' && !cancelled() }} runs-on: ubuntu-latest needs: [ "python" ] steps: