mirror of
https://github.com/RafaelSolVargas/Vulkan.git
synced 2025-10-29 16:57:23 +00:00
237 lines
8.3 KiB
Python
237 lines
8.3 KiB
Python
import asyncio
|
|
from os import listdir
|
|
from discord import Intents
|
|
from asyncio import AbstractEventLoop, Semaphore
|
|
from multiprocessing import Process, Queue
|
|
from threading import Lock, Thread
|
|
from typing import Callable, Text
|
|
from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
|
|
from discord.ext.commands import Context
|
|
from Music.Playlist import Playlist
|
|
from Music.Song import Song
|
|
from Config.Configs import Configs
|
|
from discord.ext.commands import Bot
|
|
from Parallelism.Commands import VCommands, VCommandsType
|
|
|
|
|
|
class TimeoutClock:
|
|
def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop):
|
|
self.__callback = callback
|
|
self.__task = loop.create_task(self.__executor())
|
|
|
|
async def __executor(self):
|
|
await asyncio.sleep(Configs().VC_TIMEOUT)
|
|
await self.__callback()
|
|
|
|
def cancel(self):
|
|
self.__task.cancel()
|
|
|
|
|
|
class PlayerProcess(Process):
|
|
"""Process that will play songs, receive commands by a received Queue"""
|
|
|
|
def __init__(self, playlist: Playlist, lock: Lock, queue: Queue) -> None:
|
|
Process.__init__(self, group=None, target=None, args=(), kwargs={})
|
|
self.__playlist: Playlist = playlist
|
|
self.__lock: Lock = lock
|
|
self.__queue: Queue = queue
|
|
|
|
# All information of discord context will be retrieved directly with discord API
|
|
self.__guild: Guild = None
|
|
self.__bot: Client = None
|
|
self.__voiceChannel: VoiceChannel = None
|
|
self.__textChannel: TextChannel = None
|
|
self.__loop: AbstractEventLoop = None
|
|
self.__configs: Configs = None
|
|
|
|
self.__playing = False
|
|
|
|
# Flag to control if the player should stop totally the playing
|
|
self.__forceStop = False
|
|
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
|
|
'options': '-vn'}
|
|
|
|
def run(self) -> None:
|
|
"""Function called in process.start(), this will exec the actually _run method it in event loop"""
|
|
print('Run')
|
|
|
|
self.__loop = asyncio.get_event_loop()
|
|
self.__configs = Configs()
|
|
|
|
# self.__loop = self.__bot.loop
|
|
self.__semStopPlaying = Semaphore(0)
|
|
self.__stopped = asyncio.Event()
|
|
# task = self.__loop.create_task(self._run())
|
|
self.__loop.run_until_complete(self._run())
|
|
|
|
async def _run(self) -> None:
|
|
# Recreate the bot instance in this new process
|
|
self.__bot = await self.__createBotInstance()
|
|
|
|
# Start the timeout function
|
|
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
|
|
# Thread that will receive commands to execute in this Process
|
|
self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True)
|
|
self.__commandsReceiver.start()
|
|
|
|
# Start a Task to play songs
|
|
self.__loop.create_task(self.__playPlaylistSongs())
|
|
# Try to acquire a semaphore, it'll be release when timeout function trigger, we use the Semaphore
|
|
# from the asyncio lib to not block the event loop
|
|
await self.__semStopPlaying.acquire()
|
|
|
|
async def __playPlaylistSongs(self) -> None:
|
|
if not self.__playing:
|
|
with self.__lock:
|
|
song = self.__playlist.next_song()
|
|
|
|
await self.__playSong(song)
|
|
|
|
async def __playSong(self, song: Song) -> None:
|
|
try:
|
|
source = await self.__ensureSource(song)
|
|
if source is None:
|
|
self.__playNext(None, self.__context)
|
|
self.__playing = True
|
|
|
|
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
|
|
voice = self.__guild.voice_client
|
|
voice.play(player, after=lambda e: self.__playNext(e, self.__context))
|
|
|
|
self.__timer.cancel()
|
|
self.__timer = TimeoutClock(self.__timeout_handler)
|
|
|
|
await self.__context.invoke(self.__bot.get_command('np'))
|
|
except:
|
|
self.__playNext(None)
|
|
|
|
def __playNext(self, error) -> None:
|
|
if self.__forceStop: # If it's forced to stop player
|
|
self.__forceStop = False
|
|
return None
|
|
|
|
with self.__lock:
|
|
song = self.__playlist.next_song()
|
|
|
|
if song is not None:
|
|
coro = self.__playSong(song)
|
|
self.__bot.loop.create_task(coro)
|
|
else:
|
|
self.__playing = False
|
|
|
|
def __commandsReceiver(self) -> None:
|
|
for x in range(2):
|
|
command: VCommands = self.__queue.get()
|
|
type = command.getType()
|
|
args = command.getArgs()
|
|
|
|
if type == VCommandsType.PAUSE:
|
|
self.pause()
|
|
elif type == VCommandsType.PLAY:
|
|
self.__loop.create_task(self.__playPlaylistSongs())
|
|
elif type == VCommandsType.PLAY_PREV:
|
|
self.__playPrev()
|
|
elif type == VCommandsType.RESUME:
|
|
pass
|
|
elif type == VCommandsType.SKIP:
|
|
pass
|
|
else:
|
|
print(f'[ERROR] -> Unknown Command Received: {command}')
|
|
|
|
def pause(self):
|
|
print(id(self))
|
|
|
|
async def __playPrev(self, ctx: Context) -> None:
|
|
with self.__lock:
|
|
song = self.__playlist.prev_song()
|
|
if song is not None:
|
|
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
|
|
# Will forbidden next_song to execute after stopping current player
|
|
self.__forceStop = True
|
|
self.__guild.voice_client.stop()
|
|
self.__playing = False
|
|
|
|
await self.__playSong(ctx, song)
|
|
|
|
async def __forceStop(self) -> None:
|
|
try:
|
|
if self.__guild.voice_client is None:
|
|
return
|
|
|
|
self.__guild.voice_client.stop()
|
|
await self.__guild.voice_client.disconnect()
|
|
with self.__lock:
|
|
self.__playlist.clear()
|
|
self.__playlist.loop_off()
|
|
except Exception as e:
|
|
print(f'DEVELOPER NOTE -> Force Stop Error: {e}')
|
|
|
|
async def __createBotInstance(self) -> Client:
|
|
# Load a new bot instance, this bot should not receive commands directly
|
|
intents = Intents.default()
|
|
intents.members = True
|
|
bot = Bot(command_prefix='Rafael',
|
|
pm_help=True,
|
|
case_insensitive=True,
|
|
intents=intents)
|
|
bot.remove_command('help')
|
|
|
|
# Add the Cogs for this bot too
|
|
for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'):
|
|
print(filename)
|
|
if filename.endswith('.py'):
|
|
bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}')
|
|
|
|
# Login and connect the bot instance to discord API
|
|
task = self.__loop.create_task(bot.login(token=self.__configs.BOT_TOKEN, bot=True))
|
|
await task
|
|
self.__loop.create_task(bot.connect(reconnect=True))
|
|
# Sleep to wait connection to be established
|
|
await asyncio.sleep(1)
|
|
|
|
self.__guild: Guild = bot.get_guild(651983781258985484)
|
|
self.__voiceChannel = self.__bot.get_channel(933437427350118450)
|
|
|
|
return bot
|
|
|
|
async def __timeoutHandler(self) -> None:
|
|
if self.__guild.voice_client is None:
|
|
return
|
|
|
|
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
|
|
self.__timer = TimeoutClock(self.__timeoutHandler)
|
|
|
|
elif self.__guild.voice_client.is_connected():
|
|
with self.__lock:
|
|
self.__playlist.clear()
|
|
self.__playlist.loop_off()
|
|
await self.__guild.voice_client.disconnect()
|
|
# Release semaphore to finish process
|
|
self.__semStopPlaying.release()
|
|
|
|
async def __ensureSource(self, song: Song) -> str:
|
|
while True:
|
|
if song.source is not None: # If song got downloaded
|
|
return song.source
|
|
|
|
if song.problematic: # If song got any error
|
|
return None
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
def __is_connected(self) -> bool:
|
|
try:
|
|
if not self.__voiceChannel.is_connected():
|
|
return False
|
|
else:
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
async def __connect(self) -> bool:
|
|
try:
|
|
await self.__voiceChannel.connect(reconnect=True, timeout=None)
|
|
return True
|
|
except:
|
|
return False
|