diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index cb4ffc8..0a44c93 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + mysql.8 true com.mysql.cj.jdbc.Driver diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 3dce9c6..db25fb7 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -5,6 +5,7 @@ diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..d6d00c6 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c13a9c8..1654ece 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ discord.py -pymysql +pymysql~=1.1.1 sqlalchemy cryptography -requests -apscheduler \ No newline at end of file +requests~=2.32.3 +apscheduler~=3.11.0 +yt-dlp~=2025.6.9 +tzlocal~=5.3.1 +PyNaCl diff --git a/scr/Modules/TMC/__pycache__/McRoll.cpython-38.pyc b/scr/Modules/TMC/__pycache__/McRoll.cpython-38.pyc deleted file mode 100644 index 3a8d65d..0000000 Binary files a/scr/Modules/TMC/__pycache__/McRoll.cpython-38.pyc and /dev/null differ diff --git a/scr/Modules/TMC/__pycache__/ParseForIssues.cpython-38.pyc b/scr/Modules/TMC/__pycache__/ParseForIssues.cpython-38.pyc deleted file mode 100644 index 6448338..0000000 Binary files a/scr/Modules/TMC/__pycache__/ParseForIssues.cpython-38.pyc and /dev/null differ diff --git a/scr/Modules/TMC/__pycache__/doym.cpython-38.pyc b/scr/Modules/TMC/__pycache__/doym.cpython-38.pyc deleted file mode 100644 index 665985e..0000000 Binary files a/scr/Modules/TMC/__pycache__/doym.cpython-38.pyc and /dev/null differ diff --git a/scr/Modules/TMC/autoReply/__pycache__/Join.cpython-38.pyc b/scr/Modules/TMC/autoReply/__pycache__/Join.cpython-38.pyc deleted file mode 100644 index d13cd0d..0000000 Binary files a/scr/Modules/TMC/autoReply/__pycache__/Join.cpython-38.pyc and /dev/null differ diff --git a/scr/Modules/TMC/autoReply/__pycache__/Leave.cpython-38.pyc b/scr/Modules/TMC/autoReply/__pycache__/Leave.cpython-38.pyc deleted file mode 100644 index d8cab23..0000000 Binary files a/scr/Modules/TMC/autoReply/__pycache__/Leave.cpython-38.pyc and /dev/null differ diff --git a/scr/__pycache__/config.cpython-313.pyc b/scr/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..863e03f Binary files /dev/null and b/scr/__pycache__/config.cpython-313.pyc differ diff --git a/scr/__pycache__/myBot.cpython-313.pyc b/scr/__pycache__/myBot.cpython-313.pyc new file mode 100644 index 0000000..0ac1a0c Binary files /dev/null and b/scr/__pycache__/myBot.cpython-313.pyc differ diff --git a/scr/modules/TMC/__pycache__/__init__.cpython-313.pyc b/scr/modules/TMC/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..dd7851a Binary files /dev/null and b/scr/modules/TMC/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/modules/TMC/autoReply/__pycache__/__init__.cpython-313.pyc b/scr/modules/TMC/autoReply/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..14e31ff Binary files /dev/null and b/scr/modules/TMC/autoReply/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/modules/TMC/autoReply/__pycache__/join.cpython-313.pyc b/scr/modules/TMC/autoReply/__pycache__/join.cpython-313.pyc new file mode 100644 index 0000000..65ce9d3 Binary files /dev/null and b/scr/modules/TMC/autoReply/__pycache__/join.cpython-313.pyc differ diff --git a/scr/modules/TMC/autoReply/__pycache__/leave.cpython-313.pyc b/scr/modules/TMC/autoReply/__pycache__/leave.cpython-313.pyc new file mode 100644 index 0000000..bd97558 Binary files /dev/null and b/scr/modules/TMC/autoReply/__pycache__/leave.cpython-313.pyc differ diff --git a/scr/modules/TMC/commands/__init__.py b/scr/modules/TMC/commands/__init__.py index 3f53055..c59908c 100644 --- a/scr/modules/TMC/commands/__init__.py +++ b/scr/modules/TMC/commands/__init__.py @@ -1,6 +1,6 @@ from myBot import MyBot -cogs = [".mcRoll"] +cogs = [".mcRoll", ".player"] async def setup(bot: MyBot): diff --git a/scr/modules/TMC/commands/__pycache__/__init__.cpython-313.pyc b/scr/modules/TMC/commands/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..57bbd44 Binary files /dev/null and b/scr/modules/TMC/commands/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/modules/TMC/commands/__pycache__/mcRoll.cpython-313.pyc b/scr/modules/TMC/commands/__pycache__/mcRoll.cpython-313.pyc new file mode 100644 index 0000000..b607a85 Binary files /dev/null and b/scr/modules/TMC/commands/__pycache__/mcRoll.cpython-313.pyc differ diff --git a/scr/modules/TMC/commands/__pycache__/player.cpython-313.pyc b/scr/modules/TMC/commands/__pycache__/player.cpython-313.pyc new file mode 100644 index 0000000..38a0b5d Binary files /dev/null and b/scr/modules/TMC/commands/__pycache__/player.cpython-313.pyc differ diff --git a/scr/modules/TMC/commands/player.py b/scr/modules/TMC/commands/player.py new file mode 100644 index 0000000..c85f1e6 --- /dev/null +++ b/scr/modules/TMC/commands/player.py @@ -0,0 +1,278 @@ +import asyncio +import re +from typing import Optional + +import yt_dlp +from discord import VoiceChannel, Guild, FFmpegPCMAudio +from discord.ext import commands +from discord.ext.commands import Context + +from myBot import MyBot + +FFMPEG_OPTIONS = { + 'before_options': + '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -probesize 200M', + 'options': '-vn' +} + +YDL_OPTS = { + 'format': 'bestaudio/best', + 'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s', + 'quiet': True, + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }], +} + + +class Track: + def __init__(self, url: str, title: str): + self.url = url + self.title = title + + +class Queue: + def __init__(self, channel: VoiceChannel, bot: MyBot, cleanup_callback: callable): + self.channel = channel + self.tracks: list[Track] = [] + self.current: Optional[Track] = None + self.voice_client = None + self.loop = False + self.lock = asyncio.Lock() + self.bot = bot + self.cleanup_callback = cleanup_callback + + def is_playing(self): + return self.voice_client and self.voice_client.is_playing() + + async def play_next(self): + async with self.lock: + if self.loop and self.current: + self._play(self.current) + return + elif self.tracks: + self.current = self.tracks.pop(0) + self._play(self.current) + return + else: + self.current = None + + await asyncio.sleep(5) # Give some time for the buffer to finish before disconnecting. + await self.cleanup_callback() + + def _play(self, track: Track): + source = FFmpegPCMAudio(track.url, **FFMPEG_OPTIONS) + self.voice_client.play(source, bitrate=256, after=lambda _: self.bot.loop.call_soon_threadsafe( + asyncio.create_task, self.play_next() + )) + + +class Player(commands.Cog): + def __init__(self, bot: MyBot): + self.bot = bot + self.queues: dict[Guild, Queue] = {} + + def get_queue(self, ctx) -> Optional[Queue]: + return self.queues.get(ctx.guild) + + @commands.command() + async def play(self, ctx: Context, url: Optional[str]): + """ + Adds a song to the queue. Connects the bot if currently not connected. Supports URL's or uploaded audio files. + """ + voice = ctx.author.voice + if not voice: + await ctx.send("You must be in a voice channel!") + return + + # Determine source: attachment or URL + attachment = ctx.message.attachments[0] if ctx.message.attachments else None + if attachment: + audio_url = attachment.url + title = attachment.filename + elif url: + # Handle YouTube URL + with yt_dlp.YoutubeDL(YDL_OPTS) as ydl: + info = ydl.extract_info(url, download=False) + audio_url = info['url'] + title = info.get('title', 'Unknown Title') + else: + await ctx.send("Please provide a URL or attach an audio file.") + return + + # Get or create queue + queue = self.get_queue(ctx) + if not queue: + async def cleanup_callback(): + await self.disconnect(ctx) + + queue = Queue(voice.channel, self.bot, cleanup_callback) + self.queues[ctx.guild] = queue + queue.voice_client = await voice.channel.connect() + + # Enqueue the track + track = Track(audio_url, title) + queue.tracks.append(track) + + # Play if idle + if not queue.is_playing() and not queue.current: + await queue.play_next() + + await ctx.send(f"Queued: **{title}**") + + @commands.command() + async def queue(self, ctx: Context): + """ + Sends the queue as a message. + """ + queue = self.get_queue(ctx) + if not queue or (not queue.tracks and not queue.current): + await ctx.send("Queue is empty.") + return + + message = "**Current:** " + (queue.current.title if queue.current else "None") + for i, track in enumerate(queue.tracks, start=1): + message += f'\n{i}: {track.title}' + await ctx.send(message) + + @commands.command() + async def pop(self, ctx: Context, index: int): + """ + Removes index `index` from the queue. + :param index: int of 1-indexed media to remove from the queue. + """ + queue = self.get_queue(ctx) + if not queue or index < 1 or index > len(queue.tracks): + await ctx.send("Invalid index") + return + removed = queue.tracks.pop(index - 1) + await ctx.send(f"Removed: **{removed.title}**") + + @commands.command() + async def playNext(self, ctx: Context, index: int): + """ + Moves the media at index `index` to the top of the queue. + :param index: int of 1-indexed media to move to the top of the queue. + """ + queue = self.get_queue(ctx) + if not queue or index < 1 or index > len(queue.tracks): + await ctx.send("Invalid index") + return + track = queue.tracks.pop(index - 1) + queue.tracks.insert(0, track) + await ctx.send(f"Moved to next: **{track.title}**") + + @commands.command() + async def skip(self, ctx: Context): + """ + Skips the current media and immediately starts playing the next one in the queue. + """ + queue = self.get_queue(ctx) + if queue and queue.is_playing(): + queue.voice_client.stop() + await ctx.send("Skipped") + else: + await ctx.send("Nothing is playing") + + @commands.command() + async def loop(self, ctx: Context): + """ + Loops the currently playing media. The media will play normally until it enters its loop time. + """ + queue = self.get_queue(ctx) + if not queue or not queue.current: + await ctx.send("Nothing is currently playing.") + return + queue.loop = not queue.loop + if queue.loop: + await ctx.send(f"Looping **{queue.current.title}**.") + else: + await ctx.send(f"Stopped looping **{queue.current.title}**.") + + @commands.command() + async def seek(self, ctx: Context, seakTime: str): + """ + Seeks the currently playing media to `seakTime` + :param seakTime: str of seak time formated as a number suffixed with a h, m, or s. + for example: `1h2m52s` or `2m 5h` are both valid. + """ + queue = self.get_queue(ctx) + if not queue or not queue.current: + await ctx.send("Nothing is currently playing.") + return + + time_sec = self.parse_time_string(seakTime) + if time_sec is None: + await ctx.send("Invalid time format.") + return + + queue.voice_client.stop() + opts = FFMPEG_OPTIONS.copy() + opts['before_options'] += f" --ss {time_sec}" + source = FFmpegPCMAudio(queue.current.url, **opts) + queue.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe( + asyncio.create_task, queue.play_next() + )) + await ctx.send(f"Seeked to {seakTime}") + + @commands.command() + async def disconnect(self, ctx: Context): + """ + Clears the queue and disconnects the bot. + """ + queue = self.get_queue(ctx) + if queue and queue.voice_client: + await queue.voice_client.disconnect() + del self.queues[ctx.guild] + await ctx.send("Disconnected and cleared the queue.") + + @commands.command() + async def info(self, ctx: Context): + """ + Sends info about the currently playing media. + """ + queue = self.get_queue(ctx) + if not queue or not queue.current: + await ctx.send("Nothing is playing") + else: + await ctx.send(f"Now Playing: **{queue.current.title}**") + + @commands.command() + async def pause(self, ctx: Context): + """ + Pauses the currently playing media. + """ + queue = self.get_queue(ctx) + if queue and queue.voice_client.is_playing(): + queue.voice_client.pause() + await ctx.send("Paused") + + @commands.command() + async def unpause(self, ctx: Context): + """ + Unpauses the currently playing media. + """ + queue = self.get_queue(ctx) + if queue and queue.voice_client.is_paused(): + queue.voice_client.resume() + await ctx.send("Resumed") + + def parse_time_string(self, time_str: str) -> Optional[int]: + matches = re.findall(r'(\d+)([hms])', time_str.lower().replace(" ", '')) + if not matches: + return None + seconds = 0 + for value, unit in matches: + if unit == 'h': + seconds += int(value) * 3600 + elif unit == 'm': + seconds += int(value) * 60 + elif unit == 's': + seconds += int(value) + return seconds + + +async def setup(bot: MyBot): + await bot.add_cog(Player(bot)) diff --git a/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-312.pyc b/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..955c9d4 Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-312.pyc differ diff --git a/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-313.pyc b/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..fcf25d7 Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/__init__.cpython-313.pyc differ diff --git a/scr/modules/youtrackIntegration/__pycache__/api.cpython-312.pyc b/scr/modules/youtrackIntegration/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..0206d4c Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/api.cpython-312.pyc differ diff --git a/scr/modules/youtrackIntegration/__pycache__/api.cpython-313.pyc b/scr/modules/youtrackIntegration/__pycache__/api.cpython-313.pyc new file mode 100644 index 0000000..e2f9100 Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/api.cpython-313.pyc differ diff --git a/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-312.pyc b/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-312.pyc new file mode 100644 index 0000000..dbcde05 Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-312.pyc differ diff --git a/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-313.pyc b/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-313.pyc new file mode 100644 index 0000000..6435c03 Binary files /dev/null and b/scr/modules/youtrackIntegration/__pycache__/parseForIssues.cpython-313.pyc differ diff --git a/scr/myBot.py b/scr/myBot.py index be081ba..dfd0a30 100644 --- a/scr/myBot.py +++ b/scr/myBot.py @@ -9,6 +9,8 @@ class MyBot(commands.Bot): intents = discord.Intents.default() intents.message_content = True intents.members = True + intents.voice_states = True + intents = intents.all() super().__init__(command_prefix=config.BOT.prefix, intents=intents) self.config: Config = config