From 9123144fe2288f26052cbc2132d6792d29774c0f Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 20 Jul 2024 18:45:02 -0400 Subject: [PATCH 1/7] REF: Added in Music code REF: Added in Terms of Services Code REF: Added in Data Privacy Code REF: Added the code needed for Selena to run DOC: launch.json is only for development --- .vscode/launch.json | 15 ++++ config.py | 22 ++++++ main.py | 56 +++++++++++++ modules/admin/data_privacy.py | 51 ++++++++++++ modules/admin/terms_privacy.py | 73 +++++++++++++++++ modules/music/music.py | 139 +++++++++++++++++++++++++++++++++ 6 files changed, 356 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 config.py create mode 100644 main.py create mode 100644 modules/admin/data_privacy.py create mode 100644 modules/admin/terms_privacy.py create mode 100644 modules/music/music.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..12f7d95 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Selena", + "type": "debugpy", + "request": "launch", + "program": "E:\\Development\\AI Development\\Selena\\main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b9bf8cb --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +config = { + 'DISCORD_TOKEN': os.getenv('DISCORD_TOKEN'), + 'GUILD_ID_1': int(os.getenv('DISCORD_GUILD_ID')), + 'GUILD_ID_2': int(os.getenv('DISCORD_GUILD_ID_2')), + 'DISCORD_CHANNEL_ID': int(os.getenv('DISCORD_CHANNEL_ID')), + 'YOUTUBE_API_KEY': os.getenv('YOUTUBE_API_KEY'), + 'TWITCH_CLIENT_ID': os.getenv('TWITCH_CLIENT_ID'), + 'TWITCH_CLIENT_SECRET': os.getenv('TWITCH_CLIENT_SECRET'), + 'BUNGIE_API_KEY': os.getenv('BUNGIE_API_KEY'), + 'OAUTH_URL': os.getenv('OAUTH_URL'), + 'OAUTH_CLIENT_ID': os.getenv('OAUTH_CLIENT_ID'), + 'modules': { + 'music': {'enabled': True}, + 'terms_privacy': {'enabled': True}, + 'data_privacy': {'enabled': True} + } +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..247d25a --- /dev/null +++ b/main.py @@ -0,0 +1,56 @@ +import discord +from config import config +import logging + +logging.basicConfig(level=logging.INFO) + +TOKEN = config['DISCORD_TOKEN'] +GUILD_ID_1 = config['GUILD_ID_1'] +GUILD_ID_2 = config['GUILD_ID_2'] + +intents = discord.Intents.default() +intents.message_content = True + + +class Selena(discord.Client): + def __init__(self): + super().__init__(intents=intents) + self.tree = discord.app_commands.CommandTree(self) + self.xp_module = None # Initialize as None + self.load_modules() + + async def setup_hook(self): + logging.info("Setting up modules...") + await self.tree.sync(guild=discord.Object(id=GUILD_ID_1)) + logging.info(f"Modules setup and commands synchronized to {GUILD_ID_1}") + await self.tree.sync(guild=discord.Object(id=GUILD_ID_2)) + logging.info(f"Modules setup and commands synchronized to {GUILD_ID_2}") + # Call setup_hook for xp_module here + if self.xp_module: + await self.xp_module.setup_hook() + + def load_modules(self): + if config['modules']['music']['enabled']: + from modules.music.music import setup as music_setup + music_setup(self) + logging.info("Music module loaded") + + if config['modules']['terms_privacy']['enabled']: + from modules.terms_privacy.terms_privacy import setup as terms_privacy_setup + terms_privacy_setup(self) + logging.info("Terms and Privacy module loaded") + + if config['modules']['data_privacy']['enabled']: + from modules.data_privacy.data_privacy import setup as data_privacy_setup + data_privacy_setup(self) + logging.info("Data Privacy module loaded") + + +bot = Selena() + + +@bot.event +async def on_ready(): + logging.info(f'{bot.user.name} has connected to Discord!') + +bot.run(TOKEN) diff --git a/modules/admin/data_privacy.py b/modules/admin/data_privacy.py new file mode 100644 index 0000000..e6b3013 --- /dev/null +++ b/modules/admin/data_privacy.py @@ -0,0 +1,51 @@ +import discord +from discord import app_commands +import sqlite3 + + +class DataPrivacy: + def __init__(self, bot): + self.bot = bot + self.db_path = 'data/selena.db' + + async def fetch_user_data(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM user_data WHERE user_id = ?", (user_id,)) + data = cursor.fetchall() + conn.close() + return data + + async def delete_user_data(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM user_data WHERE user_id = ?", (user_id,)) + conn.commit() + conn.close() + + def setup(self, tree: app_commands.CommandTree): + @tree.command(name="request_data", description="Request your stored data") + async def request_data_command(interaction: discord.Interaction): + user_id = interaction.user.id + data = await self.fetch_user_data(user_id) + if data: + await interaction.response.send_message(f"Your data: {data}", ephemeral=True) + else: + await interaction.response.send_message("No data found for your user.", ephemeral=True) + + @tree.command(name="delete_data", description="Request deletion of your stored data") + async def delete_data_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.delete_user_data(user_id) + await interaction.response.send_message("Your data has been deleted.", ephemeral=True) + + if not tree.get_command("request_data"): + tree.add_command(request_data_command) + + if not tree.get_command("delete_data"): + tree.add_command(delete_data_command) + + +def setup(bot): + data_privacy = DataPrivacy(bot) + data_privacy.setup(bot.tree) diff --git a/modules/admin/terms_privacy.py b/modules/admin/terms_privacy.py new file mode 100644 index 0000000..9956464 --- /dev/null +++ b/modules/admin/terms_privacy.py @@ -0,0 +1,73 @@ +import discord +from discord import app_commands +import sqlite3 + + +class TermsPrivacy: + def __init__(self, bot): + self.bot = bot + self.db_path = 'data/selena.db' + self.privacy_policy_url = "https://advtech92.github.io/selena-website/privacy_policy.html" + self.terms_of_service_url = "https://advtech92.github.io/selena-website/terms_of_service.html" + + async def user_opt_out(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("INSERT INTO opt_out_users (user_id) VALUES (?)", (user_id,)) + conn.commit() + conn.close() + + async def user_opt_in(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM opt_out_users WHERE user_id = ?", (user_id,)) + conn.commit() + conn.close() + + async def is_user_opted_out(self, user_id): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT 1 FROM opt_out_users WHERE user_id = ?", (user_id,)) + result = cursor.fetchone() + conn.close() + return result is not None + + def setup(self, tree: app_commands.CommandTree): + @tree.command(name="privacy_policy", description="Show the privacy policy") + async def privacy_policy_command(interaction: discord.Interaction): + embed = discord.Embed(title="Privacy Policy", url=self.privacy_policy_url, description="Read our privacy policy.", color=discord.Color.blue()) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @tree.command(name="terms_of_service", description="Show the terms of service") + async def terms_of_service_command(interaction: discord.Interaction): + embed = discord.Embed(title="Terms of Service", url=self.terms_of_service_url, description="Read our terms of service.", color=discord.Color.blue()) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @tree.command(name="opt_out", description="Opt out of using the bot") + async def opt_out_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.user_opt_out(user_id) + await interaction.response.send_message("You have opted out of using the bot.", ephemeral=True) + + @tree.command(name="opt_in", description="Opt back in to using the bot") + async def opt_in_command(interaction: discord.Interaction): + user_id = interaction.user.id + await self.user_opt_in(user_id) + await interaction.response.send_message("You have opted back in to using the bot.", ephemeral=True) + + if not tree.get_command("privacy_policy"): + tree.add_command(privacy_policy_command) + + if not tree.get_command("terms_of_service"): + tree.add_command(terms_of_service_command) + + if not tree.get_command("opt_out"): + tree.add_command(opt_out_command) + + if not tree.get_command("opt_in"): + tree.add_command(opt_in_command) + + +def setup(bot): + terms_privacy = TermsPrivacy(bot) + terms_privacy.setup(bot.tree) diff --git a/modules/music/music.py b/modules/music/music.py new file mode 100644 index 0000000..9d4da89 --- /dev/null +++ b/modules/music/music.py @@ -0,0 +1,139 @@ +import discord +from discord.ext import commands +from discord import app_commands +from yt_dlp import YoutubeDL +import os +from dotenv import load_dotenv + +load_dotenv() + +YTDL_OPTIONS = { + 'format': 'bestaudio', + 'noplaylist': 'True', +} + +FFMPEG_OPTIONS = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn' +} + +ytdl = YoutubeDL(YTDL_OPTIONS) + + +class Music(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.queue = [] + self.is_playing = False + self.volume = 0.3 + + async def join(self, interaction: discord.Interaction): + channel = interaction.user.voice.channel + if interaction.guild.voice_client is None: + await channel.connect(self_deaf=True) # Join the channel deafened + await interaction.followup.send("Joined the voice channel.") + else: + await interaction.followup.send("Already in a voice channel.") + + async def leave(self, interaction: discord.Interaction): + if interaction.guild.voice_client: + await interaction.guild.voice_client.disconnect() + await interaction.followup.send("Left the voice channel.") + else: + await interaction.followup.send("Not connected to a voice channel.") + + async def play(self, interaction: discord.Interaction, search: str): + if not interaction.guild.voice_client: + await self.join(interaction) + + info = ytdl.extract_info(f"ytsearch:{search}", download=False)['entries'][0] + url = info['url'] + + if interaction.guild.voice_client.is_playing(): + self.queue.append((url, info['title'])) + await interaction.followup.send(f'Queued: {info["title"]}') + else: + source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) + interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) + self.is_playing = True + await interaction.followup.send(f'Playing: {info["title"]}') + + async def play_next(self, interaction: discord.Interaction): + if self.queue: + url, title = self.queue.pop(0) + source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) + interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) + await interaction.followup.send(f'Playing next: {title}') + else: + self.is_playing = False + + async def pause(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_playing(): + interaction.guild.voice_client.pause() + await interaction.followup.send("Paused the song.") + else: + await interaction.followup.send("No song is currently playing.") + + async def resume(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_paused(): + interaction.guild.voice_client.resume() + await interaction.followup.send("Resumed the song.") + else: + await interaction.followup.send("The song is not paused.") + + async def stop(self, interaction: discord.Interaction): + if interaction.guild.voice_client.is_playing(): + interaction.guild.voice_client.stop() + self.queue = [] + await interaction.followup.send("Stopped the song.") + else: + await interaction.followup.send("No song is currently playing.") + + async def set_volume(self, interaction: discord.Interaction, volume: float): + self.volume = volume + if interaction.guild.voice_client and interaction.guild.voice_client.source: + interaction.guild.voice_client.source.volume = self.volume + await interaction.followup.send(f"Volume set to {volume * 100}%.") + else: + await interaction.followup.send("No audio source found.") + + def setup(self, tree: discord.app_commands.CommandTree): + @tree.command(name="join", description="Join the voice channel") + async def join_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.join(interaction) + + @tree.command(name="leave", description="Leave the voice channel") + async def leave_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.leave(interaction) + + @tree.command(name="play", description="Play a song from YouTube") + async def play_command(interaction: discord.Interaction, search: str): + await interaction.response.defer() + await self.play(interaction, search) + + @tree.command(name="pause", description="Pause the current song") + async def pause_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.pause(interaction) + + @tree.command(name="resume", description="Resume the paused song") + async def resume_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.resume(interaction) + + @tree.command(name="stop", description="Stop the current song") + async def stop_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.stop(interaction) + + @tree.command(name="volume", description="Set the volume (0 to 1)") + async def volume_command(interaction: discord.Interaction, volume: float): + await interaction.response.defer() + await self.set_volume(interaction, volume) + + +def setup(bot): + music = Music(bot) + music.setup(bot.tree) From baade2c14813d925025136f098df6d577a2cf7cf Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 20 Jul 2024 18:46:22 -0400 Subject: [PATCH 2/7] REF: Forgot to add the Github sync alert --- .github/workflows/discord_sync.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/discord_sync.yml diff --git a/.github/workflows/discord_sync.yml b/.github/workflows/discord_sync.yml new file mode 100644 index 0000000..6a92ee8 --- /dev/null +++ b/.github/workflows/discord_sync.yml @@ -0,0 +1,15 @@ +name: Discord Webhook + +on: [push] + +jobs: + git: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + - name: Run Discord Webhook + uses: johnnyhuy/actions-discord-git-webhook@main + with: + webhook_url: ${{ secrets.YOUR_DISCORD_WEBHOOK_URL }} From b18ab588761e31f492ade6fb020084899cebccd8 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 14 Aug 2024 13:52:20 -0400 Subject: [PATCH 3/7] FIX: Corrected an import issue in main.py FEAT: Added a loop function to the music module. --- main.py | 4 ++-- modules/music/music.py | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 247d25a..54e9245 100644 --- a/main.py +++ b/main.py @@ -36,12 +36,12 @@ class Selena(discord.Client): logging.info("Music module loaded") if config['modules']['terms_privacy']['enabled']: - from modules.terms_privacy.terms_privacy import setup as terms_privacy_setup + from modules.admin.terms_privacy import setup as terms_privacy_setup terms_privacy_setup(self) logging.info("Terms and Privacy module loaded") if config['modules']['data_privacy']['enabled']: - from modules.data_privacy.data_privacy import setup as data_privacy_setup + from modules.admin.data_privacy import setup as data_privacy_setup data_privacy_setup(self) logging.info("Data Privacy module loaded") diff --git a/modules/music/music.py b/modules/music/music.py index 9d4da89..fe29351 100644 --- a/modules/music/music.py +++ b/modules/music/music.py @@ -19,13 +19,14 @@ FFMPEG_OPTIONS = { ytdl = YoutubeDL(YTDL_OPTIONS) - class Music(commands.Cog): def __init__(self, bot): self.bot = bot self.queue = [] self.is_playing = False self.volume = 0.3 + self.loop = False # Initialize loop state + self.current_song = None # Track the current song async def join(self, interaction: discord.Interaction): channel = interaction.user.voice.channel @@ -53,14 +54,21 @@ class Music(commands.Cog): self.queue.append((url, info['title'])) await interaction.followup.send(f'Queued: {info["title"]}') else: + self.current_song = (url, info['title']) source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) self.is_playing = True await interaction.followup.send(f'Playing: {info["title"]}') async def play_next(self, interaction: discord.Interaction): - if self.queue: + if self.loop and self.current_song: # If loop is active, repeat the current song + url, title = self.current_song + source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) + interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) + await interaction.followup.send(f'Repeating: {title}') + elif self.queue: url, title = self.queue.pop(0) + self.current_song = (url, title) source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) await interaction.followup.send(f'Playing next: {title}') @@ -85,6 +93,7 @@ class Music(commands.Cog): if interaction.guild.voice_client.is_playing(): interaction.guild.voice_client.stop() self.queue = [] + self.current_song = None await interaction.followup.send("Stopped the song.") else: await interaction.followup.send("No song is currently playing.") @@ -97,6 +106,11 @@ class Music(commands.Cog): else: await interaction.followup.send("No audio source found.") + async def toggle_loop(self, interaction: discord.Interaction): + self.loop = not self.loop # Toggle the loop state + state = "enabled" if self.loop else "disabled" + await interaction.followup.send(f"Loop has been {state}.") + def setup(self, tree: discord.app_commands.CommandTree): @tree.command(name="join", description="Join the voice channel") async def join_command(interaction: discord.Interaction): @@ -133,6 +147,11 @@ class Music(commands.Cog): await interaction.response.defer() await self.set_volume(interaction, volume) + @tree.command(name="loop", description="Toggle loop for the current song") + async def loop_command(interaction: discord.Interaction): + await interaction.response.defer() + await self.toggle_loop(interaction) + def setup(bot): music = Music(bot) From ae3a610b33d66a3b4a0bd55ab734240df300e1c5 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 14 Aug 2024 14:05:20 -0400 Subject: [PATCH 4/7] FEAT: Attemping to still add loop FIX: Did some REF of the main.py to make more sense. --- main.py | 15 +++++++++------ modules/music/music.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 54e9245..521fec5 100644 --- a/main.py +++ b/main.py @@ -31,18 +31,21 @@ class Selena(discord.Client): def load_modules(self): if config['modules']['music']['enabled']: - from modules.music.music import setup as music_setup - music_setup(self) + from modules.music.music import Music + music = Music(self) + music.setup(self.tree) logging.info("Music module loaded") if config['modules']['terms_privacy']['enabled']: - from modules.admin.terms_privacy import setup as terms_privacy_setup - terms_privacy_setup(self) + from modules.admin.terms_privacy import TermsPrivacy + terms_privacy = TermsPrivacy(self) + terms_privacy.setup(self.tree) logging.info("Terms and Privacy module loaded") if config['modules']['data_privacy']['enabled']: - from modules.admin.data_privacy import setup as data_privacy_setup - data_privacy_setup(self) + from modules.admin.data_privacy import DataPrivacy + data_privacy = DataPrivacy(self) + data_privacy.setup(self.tree) logging.info("Data Privacy module loaded") diff --git a/modules/music/music.py b/modules/music/music.py index fe29351..e16104d 100644 --- a/modules/music/music.py +++ b/modules/music/music.py @@ -19,6 +19,7 @@ FFMPEG_OPTIONS = { ytdl = YoutubeDL(YTDL_OPTIONS) + class Music(commands.Cog): def __init__(self, bot): self.bot = bot @@ -152,6 +153,30 @@ class Music(commands.Cog): await interaction.response.defer() await self.toggle_loop(interaction) + if not tree.get_command("play"): + tree.add_command(play_command) + + if not tree.get_command("pause"): + tree.add_command(pause_command) + + if not tree.get_command("resume"): + tree.add_command(resume_command) + + if not tree.get_command("stop"): + tree.add_command(stop_command) + + if not tree.get_command("volume"): + tree.add_command(volume_command) + + if not tree.get_command("loop"): + tree.add_command(loop_command) + + if not tree.get_command("join"): + tree.add_command(join_command) + + if not tree.get_command("leave"): + tree.add_command(leave_command) + def setup(bot): music = Music(bot) From 1b5587c958da43235a8167eee22466f9f2133580 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Sep 2024 10:22:43 -0400 Subject: [PATCH 5/7] Redid the code completely for Selena's music --- config.py | 22 --- main.py | 63 +++----- modules/admin/data_privacy.py | 51 ------- modules/admin/terms_privacy.py | 73 ---------- modules/music.py | 256 +++++++++++++++++++++++++++++++++ modules/music/music.py | 183 ----------------------- music_state.json | 1 + 7 files changed, 276 insertions(+), 373 deletions(-) delete mode 100644 config.py delete mode 100644 modules/admin/data_privacy.py delete mode 100644 modules/admin/terms_privacy.py create mode 100644 modules/music.py delete mode 100644 modules/music/music.py create mode 100644 music_state.json diff --git a/config.py b/config.py deleted file mode 100644 index b9bf8cb..0000000 --- a/config.py +++ /dev/null @@ -1,22 +0,0 @@ -from dotenv import load_dotenv -import os - -load_dotenv() - -config = { - 'DISCORD_TOKEN': os.getenv('DISCORD_TOKEN'), - 'GUILD_ID_1': int(os.getenv('DISCORD_GUILD_ID')), - 'GUILD_ID_2': int(os.getenv('DISCORD_GUILD_ID_2')), - 'DISCORD_CHANNEL_ID': int(os.getenv('DISCORD_CHANNEL_ID')), - 'YOUTUBE_API_KEY': os.getenv('YOUTUBE_API_KEY'), - 'TWITCH_CLIENT_ID': os.getenv('TWITCH_CLIENT_ID'), - 'TWITCH_CLIENT_SECRET': os.getenv('TWITCH_CLIENT_SECRET'), - 'BUNGIE_API_KEY': os.getenv('BUNGIE_API_KEY'), - 'OAUTH_URL': os.getenv('OAUTH_URL'), - 'OAUTH_CLIENT_ID': os.getenv('OAUTH_CLIENT_ID'), - 'modules': { - 'music': {'enabled': True}, - 'terms_privacy': {'enabled': True}, - 'data_privacy': {'enabled': True} - } -} diff --git a/main.py b/main.py index 521fec5..fd3dcab 100644 --- a/main.py +++ b/main.py @@ -1,59 +1,34 @@ import discord -from config import config -import logging +from discord.ext import commands +import asyncio +import os +from dotenv import load_dotenv -logging.basicConfig(level=logging.INFO) +# Load environment variables +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') -TOKEN = config['DISCORD_TOKEN'] -GUILD_ID_1 = config['GUILD_ID_1'] -GUILD_ID_2 = config['GUILD_ID_2'] +# Import the Music module +from modules.music import Music intents = discord.Intents.default() -intents.message_content = True +intents.message_content = True # Required for accessing message content class Selena(discord.Client): - def __init__(self): + def __init__(self, *, intents): super().__init__(intents=intents) self.tree = discord.app_commands.CommandTree(self) - self.xp_module = None # Initialize as None - self.load_modules() + + # Initialize modules + self.music = Music(self) async def setup_hook(self): - logging.info("Setting up modules...") - await self.tree.sync(guild=discord.Object(id=GUILD_ID_1)) - logging.info(f"Modules setup and commands synchronized to {GUILD_ID_1}") - await self.tree.sync(guild=discord.Object(id=GUILD_ID_2)) - logging.info(f"Modules setup and commands synchronized to {GUILD_ID_2}") - # Call setup_hook for xp_module here - if self.xp_module: - await self.xp_module.setup_hook() - - def load_modules(self): - if config['modules']['music']['enabled']: - from modules.music.music import Music - music = Music(self) - music.setup(self.tree) - logging.info("Music module loaded") - - if config['modules']['terms_privacy']['enabled']: - from modules.admin.terms_privacy import TermsPrivacy - terms_privacy = TermsPrivacy(self) - terms_privacy.setup(self.tree) - logging.info("Terms and Privacy module loaded") - - if config['modules']['data_privacy']['enabled']: - from modules.admin.data_privacy import DataPrivacy - data_privacy = DataPrivacy(self) - data_privacy.setup(self.tree) - logging.info("Data Privacy module loaded") + # Sync the app commands with Discord + await self.tree.sync() -bot = Selena() +client = Selena(intents=intents) - -@bot.event -async def on_ready(): - logging.info(f'{bot.user.name} has connected to Discord!') - -bot.run(TOKEN) +# Run the bot +client.run(TOKEN) diff --git a/modules/admin/data_privacy.py b/modules/admin/data_privacy.py deleted file mode 100644 index e6b3013..0000000 --- a/modules/admin/data_privacy.py +++ /dev/null @@ -1,51 +0,0 @@ -import discord -from discord import app_commands -import sqlite3 - - -class DataPrivacy: - def __init__(self, bot): - self.bot = bot - self.db_path = 'data/selena.db' - - async def fetch_user_data(self, user_id): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("SELECT * FROM user_data WHERE user_id = ?", (user_id,)) - data = cursor.fetchall() - conn.close() - return data - - async def delete_user_data(self, user_id): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("DELETE FROM user_data WHERE user_id = ?", (user_id,)) - conn.commit() - conn.close() - - def setup(self, tree: app_commands.CommandTree): - @tree.command(name="request_data", description="Request your stored data") - async def request_data_command(interaction: discord.Interaction): - user_id = interaction.user.id - data = await self.fetch_user_data(user_id) - if data: - await interaction.response.send_message(f"Your data: {data}", ephemeral=True) - else: - await interaction.response.send_message("No data found for your user.", ephemeral=True) - - @tree.command(name="delete_data", description="Request deletion of your stored data") - async def delete_data_command(interaction: discord.Interaction): - user_id = interaction.user.id - await self.delete_user_data(user_id) - await interaction.response.send_message("Your data has been deleted.", ephemeral=True) - - if not tree.get_command("request_data"): - tree.add_command(request_data_command) - - if not tree.get_command("delete_data"): - tree.add_command(delete_data_command) - - -def setup(bot): - data_privacy = DataPrivacy(bot) - data_privacy.setup(bot.tree) diff --git a/modules/admin/terms_privacy.py b/modules/admin/terms_privacy.py deleted file mode 100644 index 9956464..0000000 --- a/modules/admin/terms_privacy.py +++ /dev/null @@ -1,73 +0,0 @@ -import discord -from discord import app_commands -import sqlite3 - - -class TermsPrivacy: - def __init__(self, bot): - self.bot = bot - self.db_path = 'data/selena.db' - self.privacy_policy_url = "https://advtech92.github.io/selena-website/privacy_policy.html" - self.terms_of_service_url = "https://advtech92.github.io/selena-website/terms_of_service.html" - - async def user_opt_out(self, user_id): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("INSERT INTO opt_out_users (user_id) VALUES (?)", (user_id,)) - conn.commit() - conn.close() - - async def user_opt_in(self, user_id): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("DELETE FROM opt_out_users WHERE user_id = ?", (user_id,)) - conn.commit() - conn.close() - - async def is_user_opted_out(self, user_id): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("SELECT 1 FROM opt_out_users WHERE user_id = ?", (user_id,)) - result = cursor.fetchone() - conn.close() - return result is not None - - def setup(self, tree: app_commands.CommandTree): - @tree.command(name="privacy_policy", description="Show the privacy policy") - async def privacy_policy_command(interaction: discord.Interaction): - embed = discord.Embed(title="Privacy Policy", url=self.privacy_policy_url, description="Read our privacy policy.", color=discord.Color.blue()) - await interaction.response.send_message(embed=embed, ephemeral=True) - - @tree.command(name="terms_of_service", description="Show the terms of service") - async def terms_of_service_command(interaction: discord.Interaction): - embed = discord.Embed(title="Terms of Service", url=self.terms_of_service_url, description="Read our terms of service.", color=discord.Color.blue()) - await interaction.response.send_message(embed=embed, ephemeral=True) - - @tree.command(name="opt_out", description="Opt out of using the bot") - async def opt_out_command(interaction: discord.Interaction): - user_id = interaction.user.id - await self.user_opt_out(user_id) - await interaction.response.send_message("You have opted out of using the bot.", ephemeral=True) - - @tree.command(name="opt_in", description="Opt back in to using the bot") - async def opt_in_command(interaction: discord.Interaction): - user_id = interaction.user.id - await self.user_opt_in(user_id) - await interaction.response.send_message("You have opted back in to using the bot.", ephemeral=True) - - if not tree.get_command("privacy_policy"): - tree.add_command(privacy_policy_command) - - if not tree.get_command("terms_of_service"): - tree.add_command(terms_of_service_command) - - if not tree.get_command("opt_out"): - tree.add_command(opt_out_command) - - if not tree.get_command("opt_in"): - tree.add_command(opt_in_command) - - -def setup(bot): - terms_privacy = TermsPrivacy(bot) - terms_privacy.setup(bot.tree) diff --git a/modules/music.py b/modules/music.py new file mode 100644 index 0000000..50d9c1e --- /dev/null +++ b/modules/music.py @@ -0,0 +1,256 @@ +import discord +from discord import app_commands +import yt_dlp +import asyncio +import json +import os + +class Music: + def __init__(self, client): + self.client = client + self.voice_clients = {} + self.music_queues = {} + self.current_tracks = {} + self.save_file = 'music_state.json' + self.volumes = {} # Store volume levels per guild + self.default_volume = 0.5 # Default volume level (50%) + + # Load saved state for auto-resume + self.load_music_state() + + # Register app commands + self.register_commands() + + def register_commands(self): + @app_commands.command(name='play', description='Play a song by title and artist') + async def play(interaction: discord.Interaction, *, query: str): + await self.play(interaction, query) + + @app_commands.command(name='pause', description='Pause the current song') + async def pause(interaction: discord.Interaction): + await self.pause(interaction) + + @app_commands.command(name='resume', description='Resume the paused song') + async def resume(interaction: discord.Interaction): + await self.resume(interaction) + + @app_commands.command(name='skip', description='Skip the current song') + async def skip(interaction: discord.Interaction): + await self.skip(interaction) + + @app_commands.command(name='stop', description='Stop playback and clear the queue') + async def stop(interaction: discord.Interaction): + await self.stop(interaction) + + @app_commands.command(name='volume', description='Set the playback volume') + @app_commands.describe(level='Volume level between 0 and 100') + async def volume(interaction: discord.Interaction, level: int): + await self.set_volume(interaction, level) + + # Add commands to the client's tree + self.client.tree.add_command(play) + self.client.tree.add_command(pause) + self.client.tree.add_command(resume) + self.client.tree.add_command(skip) + self.client.tree.add_command(stop) + self.client.tree.add_command(volume) + + async def play(self, interaction: discord.Interaction, query: str): + await interaction.response.defer() + + guild_id = interaction.guild_id + + # Check if the user is in a voice channel + if interaction.user.voice is None: + await interaction.followup.send("You must be connected to a voice channel to use this command.") + return + + # Connect to the voice channel if not already connected + if guild_id not in self.voice_clients or not self.voice_clients[guild_id].is_connected(): + channel = interaction.user.voice.channel + self.voice_clients[guild_id] = await channel.connect() + + # Ensure volume is set + if guild_id not in self.volumes: + self.volumes[guild_id] = self.default_volume + + await interaction.followup.send(f"πŸ” **Searching for:** {query}") + + # Search YouTube for the song + search_url = await self.search_youtube(query) + if not search_url: + await interaction.followup.send("❌ **Could not find the song on YouTube.**") + return + + # Add URL to the music queue + if guild_id not in self.music_queues: + self.music_queues[guild_id] = [] + self.music_queues[guild_id].append(search_url) + + await interaction.followup.send(f"βœ… **Added to queue:** {query}") + + # If nothing is playing, start playing + if not self.voice_clients[guild_id].is_playing(): + await self.play_next(guild_id) + + async def search_youtube(self, query): + """Searches YouTube for the query and returns the URL of the first result.""" + ytdl_opts = { + 'format': 'bestaudio/best', + 'noplaylist': True, + 'default_search': 'ytsearch', + 'quiet': True, + 'no_warnings': True, + 'skip_download': True, + } + loop = asyncio.get_event_loop() + with yt_dlp.YoutubeDL(ytdl_opts) as ytdl: + try: + info = await loop.run_in_executor(None, lambda: ytdl.extract_info(query, download=False)) + if 'entries' in info: + # Take the first item from the search results + video = info['entries'][0] + else: + video = info + video_url = f"https://www.youtube.com/watch?v={video['id']}" + return video_url + except Exception as e: + print(f"Error searching YouTube: {e}") + return None + + async def play_next(self, guild_id): + if guild_id not in self.music_queues or not self.music_queues[guild_id]: + await self.voice_clients[guild_id].disconnect() + return + + url = self.music_queues[guild_id].pop(0) + self.current_tracks[guild_id] = url + + try: + # Use yt_dlp to get audio source + ytdl_opts = {'format': 'bestaudio/best'} + loop = asyncio.get_event_loop() + with yt_dlp.YoutubeDL(ytdl_opts) as ytdl: + info = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False)) + audio_url = info['url'] + title = info.get('title', 'Unknown Title') + webpage_url = info.get('webpage_url', url) + thumbnail = info.get('thumbnail') + + # Prepare FFmpeg options + ffmpeg_opts = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn', + } + + # Create audio source using FFmpegPCMAudio + source = discord.FFmpegPCMAudio(audio_url, **ffmpeg_opts) + volume = self.volumes.get(guild_id, self.default_volume) + source = discord.PCMVolumeTransformer(source, volume=volume) + + # Play audio + self.voice_clients[guild_id].play(source, after=lambda e: self.after_song(e, guild_id)) + + # Send an embedded message indicating the song is now playing + embed = discord.Embed( + title="Now Playing 🎡", + description=f"[{title}]({webpage_url})", + color=discord.Color.blue() + ) + if thumbnail: + embed.set_thumbnail(url=thumbnail) + + channel = self.voice_clients[guild_id].channel + text_channel = channel.guild.system_channel or channel.guild.text_channels[0] + await text_channel.send(embed=embed) + + # Save state for auto-resume + self.save_music_state() + + except Exception as e: + print(f"Error during playback: {e}") + await self.voice_clients[guild_id].disconnect() + + def after_song(self, error, guild_id): + if error: + print(f"Error: {error}") + coro = self.play_next(guild_id) + asyncio.run_coroutine_threadsafe(coro, self.client.loop) + + async def pause(self, interaction: discord.Interaction): + guild_id = interaction.guild_id + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing(): + self.voice_clients[guild_id].pause() + await interaction.response.send_message("⏸️ **Music paused.**") + else: + await interaction.response.send_message("❌ **No music is playing.**") + + async def resume(self, interaction: discord.Interaction): + guild_id = interaction.guild_id + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_paused(): + self.voice_clients[guild_id].resume() + await interaction.response.send_message("▢️ **Music resumed.**") + else: + await interaction.response.send_message("❌ **No music is paused.**") + + async def skip(self, interaction: discord.Interaction): + guild_id = interaction.guild_id + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing(): + self.voice_clients[guild_id].stop() + await interaction.response.send_message("⏭️ **Skipped current song.**") + else: + await interaction.response.send_message("❌ **No music is playing.**") + + async def stop(self, interaction: discord.Interaction): + guild_id = interaction.guild_id + if guild_id in self.voice_clients: + self.voice_clients[guild_id].stop() + self.music_queues[guild_id] = [] + await self.voice_clients[guild_id].disconnect() + await interaction.response.send_message("πŸ›‘ **Playback stopped and queue cleared.**") + else: + await interaction.response.send_message("❌ **No music is playing.**") + + async def set_volume(self, interaction: discord.Interaction, level: int): + guild_id = interaction.guild_id + + # Validate volume level + if level < 0 or level > 100: + await interaction.response.send_message("❌ **Volume must be between 0 and 100.**") + return + + # Set the volume + volume = level / 100 # Convert to a 0.0 - 1.0 scale + self.volumes[guild_id] = volume + + # Adjust volume if something is playing + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing(): + current_source = self.voice_clients[guild_id].source + if isinstance(current_source, discord.PCMVolumeTransformer): + current_source.volume = volume + else: + # Wrap the existing source with PCMVolumeTransformer + self.voice_clients[guild_id].source = discord.PCMVolumeTransformer(current_source, volume=volume) + + await interaction.response.send_message(f"πŸ”Š **Volume set to {level}%.**") + + def save_music_state(self): + state = { + 'current_tracks': self.current_tracks, + 'music_queues': self.music_queues, + 'volumes': self.volumes + } + with open(self.save_file, 'w') as f: + json.dump(state, f) + + def load_music_state(self): + if os.path.exists(self.save_file): + with open(self.save_file, 'r') as f: + state = json.load(f) + self.current_tracks = state.get('current_tracks', {}) + self.music_queues = state.get('music_queues', {}) + self.volumes = state.get('volumes', {}) + else: + self.current_tracks = {} + self.music_queues = {} + self.volumes = {} diff --git a/modules/music/music.py b/modules/music/music.py deleted file mode 100644 index e16104d..0000000 --- a/modules/music/music.py +++ /dev/null @@ -1,183 +0,0 @@ -import discord -from discord.ext import commands -from discord import app_commands -from yt_dlp import YoutubeDL -import os -from dotenv import load_dotenv - -load_dotenv() - -YTDL_OPTIONS = { - 'format': 'bestaudio', - 'noplaylist': 'True', -} - -FFMPEG_OPTIONS = { - 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', - 'options': '-vn' -} - -ytdl = YoutubeDL(YTDL_OPTIONS) - - -class Music(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.queue = [] - self.is_playing = False - self.volume = 0.3 - self.loop = False # Initialize loop state - self.current_song = None # Track the current song - - async def join(self, interaction: discord.Interaction): - channel = interaction.user.voice.channel - if interaction.guild.voice_client is None: - await channel.connect(self_deaf=True) # Join the channel deafened - await interaction.followup.send("Joined the voice channel.") - else: - await interaction.followup.send("Already in a voice channel.") - - async def leave(self, interaction: discord.Interaction): - if interaction.guild.voice_client: - await interaction.guild.voice_client.disconnect() - await interaction.followup.send("Left the voice channel.") - else: - await interaction.followup.send("Not connected to a voice channel.") - - async def play(self, interaction: discord.Interaction, search: str): - if not interaction.guild.voice_client: - await self.join(interaction) - - info = ytdl.extract_info(f"ytsearch:{search}", download=False)['entries'][0] - url = info['url'] - - if interaction.guild.voice_client.is_playing(): - self.queue.append((url, info['title'])) - await interaction.followup.send(f'Queued: {info["title"]}') - else: - self.current_song = (url, info['title']) - source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) - interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) - self.is_playing = True - await interaction.followup.send(f'Playing: {info["title"]}') - - async def play_next(self, interaction: discord.Interaction): - if self.loop and self.current_song: # If loop is active, repeat the current song - url, title = self.current_song - source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) - interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) - await interaction.followup.send(f'Repeating: {title}') - elif self.queue: - url, title = self.queue.pop(0) - self.current_song = (url, title) - source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS), volume=self.volume) - interaction.guild.voice_client.play(source, after=lambda e: self.bot.loop.create_task(self.play_next(interaction))) - await interaction.followup.send(f'Playing next: {title}') - else: - self.is_playing = False - - async def pause(self, interaction: discord.Interaction): - if interaction.guild.voice_client.is_playing(): - interaction.guild.voice_client.pause() - await interaction.followup.send("Paused the song.") - else: - await interaction.followup.send("No song is currently playing.") - - async def resume(self, interaction: discord.Interaction): - if interaction.guild.voice_client.is_paused(): - interaction.guild.voice_client.resume() - await interaction.followup.send("Resumed the song.") - else: - await interaction.followup.send("The song is not paused.") - - async def stop(self, interaction: discord.Interaction): - if interaction.guild.voice_client.is_playing(): - interaction.guild.voice_client.stop() - self.queue = [] - self.current_song = None - await interaction.followup.send("Stopped the song.") - else: - await interaction.followup.send("No song is currently playing.") - - async def set_volume(self, interaction: discord.Interaction, volume: float): - self.volume = volume - if interaction.guild.voice_client and interaction.guild.voice_client.source: - interaction.guild.voice_client.source.volume = self.volume - await interaction.followup.send(f"Volume set to {volume * 100}%.") - else: - await interaction.followup.send("No audio source found.") - - async def toggle_loop(self, interaction: discord.Interaction): - self.loop = not self.loop # Toggle the loop state - state = "enabled" if self.loop else "disabled" - await interaction.followup.send(f"Loop has been {state}.") - - def setup(self, tree: discord.app_commands.CommandTree): - @tree.command(name="join", description="Join the voice channel") - async def join_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.join(interaction) - - @tree.command(name="leave", description="Leave the voice channel") - async def leave_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.leave(interaction) - - @tree.command(name="play", description="Play a song from YouTube") - async def play_command(interaction: discord.Interaction, search: str): - await interaction.response.defer() - await self.play(interaction, search) - - @tree.command(name="pause", description="Pause the current song") - async def pause_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.pause(interaction) - - @tree.command(name="resume", description="Resume the paused song") - async def resume_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.resume(interaction) - - @tree.command(name="stop", description="Stop the current song") - async def stop_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.stop(interaction) - - @tree.command(name="volume", description="Set the volume (0 to 1)") - async def volume_command(interaction: discord.Interaction, volume: float): - await interaction.response.defer() - await self.set_volume(interaction, volume) - - @tree.command(name="loop", description="Toggle loop for the current song") - async def loop_command(interaction: discord.Interaction): - await interaction.response.defer() - await self.toggle_loop(interaction) - - if not tree.get_command("play"): - tree.add_command(play_command) - - if not tree.get_command("pause"): - tree.add_command(pause_command) - - if not tree.get_command("resume"): - tree.add_command(resume_command) - - if not tree.get_command("stop"): - tree.add_command(stop_command) - - if not tree.get_command("volume"): - tree.add_command(volume_command) - - if not tree.get_command("loop"): - tree.add_command(loop_command) - - if not tree.get_command("join"): - tree.add_command(join_command) - - if not tree.get_command("leave"): - tree.add_command(leave_command) - - -def setup(bot): - music = Music(bot) - music.setup(bot.tree) diff --git a/music_state.json b/music_state.json new file mode 100644 index 0000000..8f083c2 --- /dev/null +++ b/music_state.json @@ -0,0 +1 @@ +{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=fnlJw9H0xAM"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5}} \ No newline at end of file From 7c937b611287430fd1bd5bd46f012e067cfe30e0 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Sep 2024 11:27:33 -0400 Subject: [PATCH 6/7] REF: Added /Stop REF: Added /Leave FIX: Fixed music issues --- modules/music.py | 46 ++++++++++++++++++++++++++++++++++++++++------ music_state.json | 2 +- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/modules/music.py b/modules/music.py index 50d9c1e..6b4fcaa 100644 --- a/modules/music.py +++ b/modules/music.py @@ -1,3 +1,5 @@ +# modules/music.py + import discord from discord import app_commands import yt_dlp @@ -14,6 +16,7 @@ class Music: self.save_file = 'music_state.json' self.volumes = {} # Store volume levels per guild self.default_volume = 0.5 # Default volume level (50%) + self.current_interactions = {} # Store interactions per guild # Load saved state for auto-resume self.load_music_state() @@ -42,6 +45,10 @@ class Music: async def stop(interaction: discord.Interaction): await self.stop(interaction) + @app_commands.command(name='leave', description='Disconnect the bot from the voice channel') + async def leave(interaction: discord.Interaction): + await self.leave(interaction) + @app_commands.command(name='volume', description='Set the playback volume') @app_commands.describe(level='Volume level between 0 and 100') async def volume(interaction: discord.Interaction, level: int): @@ -53,6 +60,7 @@ class Music: self.client.tree.add_command(resume) self.client.tree.add_command(skip) self.client.tree.add_command(stop) + self.client.tree.add_command(leave) self.client.tree.add_command(volume) async def play(self, interaction: discord.Interaction, query: str): @@ -60,6 +68,9 @@ class Music: guild_id = interaction.guild_id + # Store the interaction object for later use + self.current_interactions[guild_id] = interaction + # Check if the user is in a voice channel if interaction.user.voice is None: await interaction.followup.send("You must be connected to a voice channel to use this command.") @@ -120,7 +131,7 @@ class Music: async def play_next(self, guild_id): if guild_id not in self.music_queues or not self.music_queues[guild_id]: - await self.voice_clients[guild_id].disconnect() + # Do not disconnect here return url = self.music_queues[guild_id].pop(0) @@ -160,16 +171,32 @@ class Music: if thumbnail: embed.set_thumbnail(url=thumbnail) - channel = self.voice_clients[guild_id].channel - text_channel = channel.guild.system_channel or channel.guild.text_channels[0] - await text_channel.send(embed=embed) + # Use the stored interaction to send the message + interaction = self.current_interactions.get(guild_id) + if interaction: + try: + await interaction.followup.send(embed=embed) + except discord.HTTPException as e: + print(f"Failed to send message: {e}") + finally: + # Remove the interaction to prevent reuse + self.current_interactions.pop(guild_id, None) + else: + # Fallback to a default text channel + channel = self.voice_clients[guild_id].channel + text_channel = channel.guild.system_channel or channel.guild.text_channels[0] + try: + await text_channel.send(embed=embed) + except discord.HTTPException as e: + print(f"Failed to send message: {e}") # Save state for auto-resume self.save_music_state() except Exception as e: print(f"Error during playback: {e}") - await self.voice_clients[guild_id].disconnect() + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected(): + await self.voice_clients[guild_id].disconnect() def after_song(self, error, guild_id): if error: @@ -206,11 +233,18 @@ class Music: if guild_id in self.voice_clients: self.voice_clients[guild_id].stop() self.music_queues[guild_id] = [] - await self.voice_clients[guild_id].disconnect() await interaction.response.send_message("πŸ›‘ **Playback stopped and queue cleared.**") else: await interaction.response.send_message("❌ **No music is playing.**") + async def leave(self, interaction: discord.Interaction): + guild_id = interaction.guild_id + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected(): + await self.voice_clients[guild_id].disconnect() + await interaction.response.send_message("πŸ‘‹ **Disconnected from the voice channel.**") + else: + await interaction.response.send_message("❌ **I am not connected to a voice channel.**") + async def set_volume(self, interaction: discord.Interaction, level: int): guild_id = interaction.guild_id diff --git a/music_state.json b/music_state.json index 8f083c2..889e26e 100644 --- a/music_state.json +++ b/music_state.json @@ -1 +1 @@ -{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=fnlJw9H0xAM"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5}} \ No newline at end of file +{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=wWoQ7PFSYlk"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5, "1142517462529757184": 0.5}} \ No newline at end of file From 470fb8463d2474800c6b0a8606ac04b3fcda4e59 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Sep 2024 11:48:01 -0400 Subject: [PATCH 7/7] REF: Changed the code so it resume songs should bot crashes --- main.py | 1 + modules/music.py | 108 ++++++++++++++++++++++++++++++++++++++++++----- music_state.json | 2 +- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index fd3dcab..fe07b76 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ class Selena(discord.Client): async def setup_hook(self): # Sync the app commands with Discord + self.loop.create_task(self.music.auto_resume_playback()) await self.tree.sync() diff --git a/modules/music.py b/modules/music.py index 6b4fcaa..63039e0 100644 --- a/modules/music.py +++ b/modules/music.py @@ -24,6 +24,8 @@ class Music: # Register app commands self.register_commands() + # Removed self.client.loop.create_task(self.auto_resume_playback()) from here + def register_commands(self): @app_commands.command(name='play', description='Play a song by title and artist') async def play(interaction: discord.Interaction, *, query: str): @@ -63,6 +65,90 @@ class Music: self.client.tree.add_command(leave) self.client.tree.add_command(volume) + async def auto_resume_playback(self): + await self.client.wait_until_ready() + for guild_id_str, url in self.current_tracks.items(): + guild_id = int(guild_id_str) + guild = self.client.get_guild(guild_id) + if guild is None: + continue + + # Find a voice channel the bot was connected to + voice_channel = None + for vc in guild.voice_channels: + if guild.me in vc.members: + voice_channel = vc + break + + if voice_channel is None: + # If the bot was not in any voice channel, skip this guild + continue + + # Connect to the voice channel + try: + self.voice_clients[guild_id] = await voice_channel.connect() + except discord.ClientException: + # Already connected to a voice channel in this guild + self.voice_clients[guild_id] = guild.voice_client + + # Resume playing + await self.play_current_track(guild_id) + + async def play_current_track(self, guild_id): + url = self.current_tracks.get(str(guild_id)) + if not url: + return + + try: + # Use yt_dlp to get audio source + ytdl_opts = {'format': 'bestaudio/best'} + loop = asyncio.get_event_loop() + with yt_dlp.YoutubeDL(ytdl_opts) as ytdl: + info = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False)) + audio_url = info['url'] + title = info.get('title', 'Unknown Title') + webpage_url = info.get('webpage_url', url) + thumbnail = info.get('thumbnail') + + # Prepare FFmpeg options + ffmpeg_opts = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn', + } + + # Create audio source using FFmpegPCMAudio + source = discord.FFmpegPCMAudio(audio_url, **ffmpeg_opts) + volume = self.volumes.get(str(guild_id), self.default_volume) + source = discord.PCMVolumeTransformer(source, volume=volume) + + # Play audio + self.voice_clients[guild_id].play(source, after=lambda e: self.after_song(e, guild_id)) + + # Send an embedded message indicating the song is now playing + embed = discord.Embed( + title="Auto-Resumed Playing 🎡", + description=f"[{title}]({webpage_url})", + color=discord.Color.green() + ) + if thumbnail: + embed.set_thumbnail(url=thumbnail) + + guild = self.client.get_guild(guild_id) + if guild: + text_channel = guild.system_channel or guild.text_channels[0] + try: + await text_channel.send(embed=embed) + except discord.HTTPException as e: + print(f"Failed to send message: {e}") + + # Save state for auto-resume + self.save_music_state() + + except Exception as e: + print(f"Error during auto-resume playback: {e}") + if guild_id in self.voice_clients and self.voice_clients[guild_id].is_connected(): + await self.voice_clients[guild_id].disconnect() + async def play(self, interaction: discord.Interaction, query: str): await interaction.response.defer() @@ -83,7 +169,7 @@ class Music: # Ensure volume is set if guild_id not in self.volumes: - self.volumes[guild_id] = self.default_volume + self.volumes[str(guild_id)] = self.default_volume await interaction.followup.send(f"πŸ” **Searching for:** {query}") @@ -135,7 +221,7 @@ class Music: return url = self.music_queues[guild_id].pop(0) - self.current_tracks[guild_id] = url + self.current_tracks[str(guild_id)] = url try: # Use yt_dlp to get audio source @@ -156,7 +242,7 @@ class Music: # Create audio source using FFmpegPCMAudio source = discord.FFmpegPCMAudio(audio_url, **ffmpeg_opts) - volume = self.volumes.get(guild_id, self.default_volume) + volume = self.volumes.get(str(guild_id), self.default_volume) source = discord.PCMVolumeTransformer(source, volume=volume) # Play audio @@ -183,12 +269,13 @@ class Music: self.current_interactions.pop(guild_id, None) else: # Fallback to a default text channel - channel = self.voice_clients[guild_id].channel - text_channel = channel.guild.system_channel or channel.guild.text_channels[0] - try: - await text_channel.send(embed=embed) - except discord.HTTPException as e: - print(f"Failed to send message: {e}") + guild = self.client.get_guild(guild_id) + if guild: + text_channel = guild.system_channel or guild.text_channels[0] + try: + await text_channel.send(embed=embed) + except discord.HTTPException as e: + print(f"Failed to send message: {e}") # Save state for auto-resume self.save_music_state() @@ -233,6 +320,7 @@ class Music: if guild_id in self.voice_clients: self.voice_clients[guild_id].stop() self.music_queues[guild_id] = [] + self.current_tracks.pop(str(guild_id), None) # Remove current track await interaction.response.send_message("πŸ›‘ **Playback stopped and queue cleared.**") else: await interaction.response.send_message("❌ **No music is playing.**") @@ -255,7 +343,7 @@ class Music: # Set the volume volume = level / 100 # Convert to a 0.0 - 1.0 scale - self.volumes[guild_id] = volume + self.volumes[str(guild_id)] = volume # Adjust volume if something is playing if guild_id in self.voice_clients and self.voice_clients[guild_id].is_playing(): diff --git a/music_state.json b/music_state.json index 889e26e..dd3e8c6 100644 --- a/music_state.json +++ b/music_state.json @@ -1 +1 @@ -{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=wWoQ7PFSYlk"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5, "1142517462529757184": 0.5}} \ No newline at end of file +{"current_tracks": {"1161168803888107550": "https://www.youtube.com/watch?v=fnlJw9H0xAM", "1142517462529757184": "https://www.youtube.com/watch?v=wWoQ7PFSYlk"}, "music_queues": {"1161168803888107550": [], "1142517462529757184": []}, "volumes": {"1161168803888107550": 0.5, "1142517462529757184": 0.5}} \ No newline at end of file