Added media player
This commit is contained in:
parent
8d37fa6355
commit
aeab204faf
2
.idea/dataSources.xml
generated
2
.idea/dataSources.xml
generated
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="@192.168.1.52" uuid="af9cb274-5470-4be5-8768-da09b50c258c">
|
||||
<data-source source="LOCAL" name="@themissingcrowbar.com" uuid="af9cb274-5470-4be5-8768-da09b50c258c">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
|
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@ -5,6 +5,7 @@
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N806" />
|
||||
<option value="N803" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
|
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/scr/modules/TMC/commands/mcRoll.py" dialect="GenericSQL" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,9 @@
|
||||
discord.py
|
||||
pymysql
|
||||
pymysql~=1.1.1
|
||||
sqlalchemy
|
||||
cryptography
|
||||
requests
|
||||
apscheduler
|
||||
requests~=2.32.3
|
||||
apscheduler~=3.11.0
|
||||
yt-dlp~=2025.6.9
|
||||
tzlocal~=5.3.1
|
||||
PyNaCl
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
scr/__pycache__/config.cpython-313.pyc
Normal file
BIN
scr/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/__pycache__/myBot.cpython-313.pyc
Normal file
BIN
scr/__pycache__/myBot.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/autoReply/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/autoReply/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/autoReply/__pycache__/join.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/autoReply/__pycache__/join.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/autoReply/__pycache__/leave.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/autoReply/__pycache__/leave.cpython-313.pyc
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
from myBot import MyBot
|
||||
|
||||
cogs = [".mcRoll"]
|
||||
cogs = [".mcRoll", ".player"]
|
||||
|
||||
|
||||
async def setup(bot: MyBot):
|
||||
|
BIN
scr/modules/TMC/commands/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/commands/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/commands/__pycache__/mcRoll.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/commands/__pycache__/mcRoll.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scr/modules/TMC/commands/__pycache__/player.cpython-313.pyc
Normal file
BIN
scr/modules/TMC/commands/__pycache__/player.cpython-313.pyc
Normal file
Binary file not shown.
278
scr/modules/TMC/commands/player.py
Normal file
278
scr/modules/TMC/commands/player.py
Normal file
@ -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))
|
Binary file not shown.
Binary file not shown.
BIN
scr/modules/youtrackIntegration/__pycache__/api.cpython-312.pyc
Normal file
BIN
scr/modules/youtrackIntegration/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scr/modules/youtrackIntegration/__pycache__/api.cpython-313.pyc
Normal file
BIN
scr/modules/youtrackIntegration/__pycache__/api.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user