Changing some methods signature in Playlist to be compatible in Shared Memory, now finishing process after some time

This commit is contained in:
Rafael Vargas 2022-07-22 18:07:44 -03:00
parent fc7de9cb4f
commit 19ae59c5b8
14 changed files with 116 additions and 103 deletions

View File

@ -10,7 +10,7 @@ class HistoryController(AbstractController):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> ControllerResponse: async def run(self) -> ControllerResponse:
history = self.player.playlist.songs_history history = self.player.playlist.getSongsHistory()
if len(history) == 0: if len(history) == 0:
text = self.messages.HISTORY_EMPTY text = self.messages.HISTORY_EMPTY

View File

@ -16,7 +16,7 @@ class LoopController(AbstractController):
return ControllerResponse(self.ctx, embed) return ControllerResponse(self.ctx, embed)
args = args.lower() args = args.lower()
if self.player.playlist.current is None: if self.player.playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)

View File

@ -33,7 +33,7 @@ class MoveController(AbstractController):
try: try:
song = self.player.playlist.move_songs(pos1, pos2) song = self.player.playlist.move_songs(pos1, pos2)
songs = self.player.playlist.songs_to_preload songs = self.player.playlist.getSongsToPreload()
await self.__down.preload(songs) await self.__down.preload(songs)
song_name = song.title if song.title else song.identifier song_name = song.title if song.title else song.identifier

View File

@ -15,12 +15,12 @@ class NowPlayingController(AbstractController):
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
return ControllerResponse(self.ctx, embed) return ControllerResponse(self.ctx, embed)
if self.player.playlist.looping_one: if self.player.playlist.isLoopingOne():
title = self.messages.ONE_SONG_LOOPING title = self.messages.ONE_SONG_LOOPING
else: else:
title = self.messages.SONG_PLAYING title = self.messages.SONG_PLAYING
await self.__cleaner.clean_messages(self.ctx, self.config.CLEANER_MESSAGES_QUANT) await self.__cleaner.clean_messages(self.ctx, self.config.CLEANER_MESSAGES_QUANT)
info = self.player.playlist.current.info info = self.player.playlist.getCurrentSong().info
embed = self.embeds.SONG_INFO(info, title) embed = self.embeds.SONG_INFO(info, title)
return ControllerResponse(self.ctx, embed) return ControllerResponse(self.ctx, embed)

View File

@ -21,7 +21,7 @@ class PlayController(AbstractController):
track = " ".join(args) track = " ".join(args)
requester = self.ctx.author.name requester = self.ctx.author.name
if not self.__user_connected(): if not self.__isUserConnected():
error = ImpossibleMove() error = ImpossibleMove()
embed = self.embeds.NO_CHANNEL() embed = self.embeds.NO_CHANNEL()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)
@ -36,7 +36,7 @@ class PlayController(AbstractController):
self.player.playlist.add_song(song) self.player.playlist.add_song(song)
quant = len(musics) quant = len(musics)
songs_preload = self.player.playlist.songs_to_preload songs_preload = self.player.playlist.getSongsToPreload()
await self.__down.preload(songs_preload) await self.__down.preload(songs_preload)
if quant == 1: if quant == 1:
@ -73,8 +73,6 @@ class PlayController(AbstractController):
queue.put(command) queue.put(command)
else: else:
# Start the process # Start the process
command = VCommands(VCommandsType.CONTEXT, self.ctx)
queue.put(command)
process.start() process.start()
return response return response
@ -91,27 +89,8 @@ class PlayController(AbstractController):
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)
def __user_connected(self) -> bool: def __isUserConnected(self) -> bool:
if self.ctx.author.voice: if self.ctx.author.voice:
return True return True
else: else:
return False return False
def __is_connected(self) -> bool:
try:
voice_channel = self.guild.voice_client.channel
if not self.guild.voice_client.is_connected():
return False
else:
return True
except:
return False
async def __connect(self) -> bool:
# if self.guild.voice_client is None:
try:
await self.ctx.author.voice.channel.connect(reconnect=True, timeout=None)
return True
except:
return False

View File

@ -27,7 +27,7 @@ class PrevController(AbstractController):
embed = self.embeds.UNKNOWN_ERROR() embed = self.embeds.UNKNOWN_ERROR()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)
if self.player.playlist.looping_all or self.player.playlist.looping_one: if self.player.playlist.isLoopingAll() or self.player.playlist.isLoopingOne():
error = BadCommandUsage() error = BadCommandUsage()
embed = self.embeds.FAIL_DUE_TO_LOOP_ON() embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)

View File

@ -5,6 +5,7 @@ from Controllers.AbstractController import AbstractController
from Controllers.ControllerResponse import ControllerResponse from Controllers.ControllerResponse import ControllerResponse
from Music.Downloader import Downloader from Music.Downloader import Downloader
from Utils.Utils import Utils from Utils.Utils import Utils
from Parallelism.ProcessManager import ProcessManager
class QueueController(AbstractController): class QueueController(AbstractController):
@ -13,32 +14,43 @@ class QueueController(AbstractController):
self.__down = Downloader() self.__down = Downloader()
async def run(self) -> ControllerResponse: async def run(self) -> ControllerResponse:
if self.player.playlist.looping_one: # Retrieve the process of the guild
song = self.player.playlist.current process = ProcessManager()
embed = self.embeds.ONE_SONG_LOOPING(song.info) processContext = process.getRunningPlayerContext(self.guild)
return ControllerResponse(self.ctx, embed) if not processContext: # If no process return empty list
songs_preload = self.player.playlist.songs_to_preload
if len(songs_preload) == 0:
embed = self.embeds.EMPTY_QUEUE() embed = self.embeds.EMPTY_QUEUE()
return ControllerResponse(self.ctx, embed) return ControllerResponse(self.ctx, embed)
asyncio.create_task(self.__down.preload(songs_preload)) # Acquire the Lock to manipulate the playlist
with processContext.getLock():
playlist = processContext.getPlaylist()
if self.player.playlist.looping_all: if playlist.isLoopingOne():
title = self.messages.ALL_SONGS_LOOPING song = playlist.getCurrentSong()
else: embed = self.embeds.ONE_SONG_LOOPING(song.info)
title = self.messages.QUEUE_TITLE return ControllerResponse(self.ctx, embed)
total_time = Utils.format_time(sum([int(song.duration if song.duration else 0) songs_preload = playlist.getSongsToPreload()
for song in songs_preload])) if len(songs_preload) == 0:
total_songs = len(self.player.playlist) embed = self.embeds.EMPTY_QUEUE()
return ControllerResponse(self.ctx, embed)
text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n' asyncio.create_task(self.__down.preload(songs_preload))
for pos, song in enumerate(songs_preload, start=1): if playlist.isLoopingAll():
song_name = song.title if song.title else self.messages.SONG_DOWNLOADING title = self.messages.ALL_SONGS_LOOPING
text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n" else:
title = self.messages.QUEUE_TITLE
embed = self.embeds.QUEUE(title, text) total_time = Utils.format_time(sum([int(song.duration if song.duration else 0)
return ControllerResponse(self.ctx, embed) for song in songs_preload]))
total_songs = len(playlist.getSongs())
text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n'
for pos, song in enumerate(songs_preload, start=1):
song_name = song.title if song.title else self.messages.SONG_DOWNLOADING
text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n"
embed = self.embeds.QUEUE(title, text)
return ControllerResponse(self.ctx, embed)

View File

@ -15,7 +15,7 @@ class ShuffleController(AbstractController):
async def run(self) -> ControllerResponse: async def run(self) -> ControllerResponse:
try: try:
self.player.playlist.shuffle() self.player.playlist.shuffle()
songs = self.player.playlist.songs_to_preload songs = self.player.playlist.getSongsToPreload()
asyncio.create_task(self.__down.preload(songs)) asyncio.create_task(self.__down.preload(songs))
embed = self.embeds.SONGS_SHUFFLED() embed = self.embeds.SONGS_SHUFFLED()

View File

@ -10,7 +10,7 @@ class SkipController(AbstractController):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> ControllerResponse: async def run(self) -> ControllerResponse:
if self.player.playlist.looping_one: if self.player.playlist.isLoopingOne():
embed = self.embeds.ERROR_DUE_LOOP_ONE_ON() embed = self.embeds.ERROR_DUE_LOOP_ONE_ON()
error = BadCommandUsage() error = BadCommandUsage()
return ControllerResponse(self.ctx, embed, error) return ControllerResponse(self.ctx, embed, error)

View File

@ -1,3 +0,0 @@
class Database:
def __init__(self) -> None:
pass

View File

@ -89,7 +89,7 @@ class Player(commands.Cog):
await ctx.invoke(self.__bot.get_command('np')) await ctx.invoke(self.__bot.get_command('np'))
songs = self.__playlist.songs_to_preload songs = self.__playlist.getSongsToPreload()
asyncio.create_task(self.__down.preload(songs)) asyncio.create_task(self.__down.preload(songs))
except: except:
self.__play_next(None, ctx) self.__play_next(None, ctx)

View File

@ -32,24 +32,19 @@ class Playlist:
return False return False
return True return True
@property def getSongsHistory(self) -> deque:
def songs_history(self) -> deque:
return self.__songs_history return self.__songs_history
@property def isLoopingOne(self) -> bool:
def looping_one(self) -> bool:
return self.__looping_one return self.__looping_one
@property def isLoopingAll(self) -> bool:
def looping_all(self) -> bool:
return self.__looping_all return self.__looping_all
@property def getCurrentSong(self) -> Song:
def current(self) -> Song:
return self.__current return self.__current
@property def getSongsToPreload(self) -> List[Song]:
def songs_to_preload(self) -> List[Song]:
return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS] return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS]
def __len__(self) -> int: def __len__(self) -> int:

View File

@ -1,10 +1,10 @@
import asyncio import asyncio
from os import listdir from os import listdir
from discord import Intents from discord import Intents, User
from asyncio import AbstractEventLoop, Semaphore from asyncio import AbstractEventLoop, Semaphore
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
from threading import Lock, Thread from threading import Lock, Thread
from typing import Callable, Text from typing import Callable
from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
from discord.ext.commands import Context from discord.ext.commands import Context
from Music.Playlist import Playlist from Music.Playlist import Playlist
@ -30,47 +30,58 @@ class TimeoutClock:
class PlayerProcess(Process): class PlayerProcess(Process):
"""Process that will play songs, receive commands by a received Queue""" """Process that will play songs, receive commands by a received Queue"""
def __init__(self, playlist: Playlist, lock: Lock, queue: Queue) -> None: def __init__(self, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None:
"""
Start a new process that will have his own bot instance
Due to pickle serialization, no objects are stored, the values initialization are being made in the run method
"""
Process.__init__(self, group=None, target=None, args=(), kwargs={}) Process.__init__(self, group=None, target=None, args=(), kwargs={})
# Synchronization objects
self.__playlist: Playlist = playlist self.__playlist: Playlist = playlist
self.__lock: Lock = lock self.__lock: Lock = lock
self.__queue: Queue = queue self.__queue: Queue = queue
self.__semStopPlaying: Semaphore = None
self.__loop: AbstractEventLoop = None
# Discord context ID
self.__textChannelID = textID
self.__guildID = guildID
self.__voiceChannelID = voiceID
self.__authorID = authorID
# All information of discord context will be retrieved directly with discord API # All information of discord context will be retrieved directly with discord API
self.__guild: Guild = None self.__guild: Guild = None
self.__bot: Client = None self.__bot: Client = None
self.__voiceChannel: VoiceChannel = None self.__voiceChannel: VoiceChannel = None
self.__textChannel: TextChannel = None self.__textChannel: TextChannel = None
self.__loop: AbstractEventLoop = None self.__author: User = None
self.__configs: Configs = None self.__configs: Configs = None
self.__playing = False self.__playing = False
# Flag to control if the player should stop totally the playing
self.__forceStop = False self.__forceStop = False
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'} 'options': '-vn'}
def run(self) -> None: def run(self) -> None:
"""Function called in process.start(), this will exec the actually _run method it in event loop""" """Method called by process.start(), this will exec the actually _run method in a event loop"""
print('Run')
self.__loop = asyncio.get_event_loop() self.__loop = asyncio.get_event_loop()
self.__configs = Configs() self.__configs = Configs()
# self.__loop = self.__bot.loop
self.__semStopPlaying = Semaphore(0) self.__semStopPlaying = Semaphore(0)
self.__stopped = asyncio.Event() self.__stopped = asyncio.Event()
# task = self.__loop.create_task(self._run())
self.__loop.run_until_complete(self._run()) self.__loop.run_until_complete(self._run())
async def _run(self) -> None: async def _run(self) -> None:
# Recreate the bot instance in this new process # Recreate the bot instance and objects using discord API
self.__bot = await self.__createBotInstance() self.__bot = await self.__createBotInstance()
self.__guild = self.__bot.get_guild(self.__guildID)
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
self.__textChannel = self.__bot.get_channel(self.__textChannelID)
self.__author = self.__bot.get_channel(self.__authorID)
# Connect to voice Channel
await self.__connectToVoiceChannel()
# Start the timeout function # Start the timeout function
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
# Thread that will receive commands to execute in this Process # Thread that will receive commands to be executed in this Process
self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True) self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True)
self.__commandsReceiver.start() self.__commandsReceiver.start()
@ -81,8 +92,10 @@ class PlayerProcess(Process):
await self.__semStopPlaying.acquire() await self.__semStopPlaying.acquire()
async def __playPlaylistSongs(self) -> None: async def __playPlaylistSongs(self) -> None:
print(f'Playing: {self.__playing}')
if not self.__playing: if not self.__playing:
with self.__lock: with self.__lock:
print('Next Song Aqui')
song = self.__playlist.next_song() song = self.__playlist.next_song()
await self.__playSong(song) await self.__playSong(song)
@ -91,18 +104,19 @@ class PlayerProcess(Process):
try: try:
source = await self.__ensureSource(song) source = await self.__ensureSource(song)
if source is None: if source is None:
self.__playNext(None, self.__context) self.__playNext(None)
self.__playing = True self.__playing = True
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
voice = self.__guild.voice_client voice = self.__guild.voice_client
voice.play(player, after=lambda e: self.__playNext(e, self.__context)) voice.play(player, after=lambda e: self.__playNext(e))
self.__timer.cancel() self.__timer.cancel()
self.__timer = TimeoutClock(self.__timeout_handler) self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
await self.__context.invoke(self.__bot.get_command('np')) # await self.__context.invoke(self.__bot.get_command('np'))
except: except Exception as e:
print(f'[ERROR IN PLAY SONG] -> {e}')
self.__playNext(None) self.__playNext(None)
def __playNext(self, error) -> None: def __playNext(self, error) -> None:
@ -120,11 +134,12 @@ class PlayerProcess(Process):
self.__playing = False self.__playing = False
def __commandsReceiver(self) -> None: def __commandsReceiver(self) -> None:
for x in range(2): while True:
command: VCommands = self.__queue.get() command: VCommands = self.__queue.get()
type = command.getType() type = command.getType()
args = command.getArgs() args = command.getArgs()
print(f'Command Received: {type}')
if type == VCommandsType.PAUSE: if type == VCommandsType.PAUSE:
self.pause() self.pause()
elif type == VCommandsType.PLAY: elif type == VCommandsType.PLAY:
@ -167,7 +182,9 @@ class PlayerProcess(Process):
print(f'DEVELOPER NOTE -> Force Stop Error: {e}') print(f'DEVELOPER NOTE -> Force Stop Error: {e}')
async def __createBotInstance(self) -> Client: async def __createBotInstance(self) -> Client:
# Load a new bot instance, this bot should not receive commands directly """Load a new bot instance that should not be directly called.
Get the guild, voice and text Channel in discord API using IDs passed in constructor
"""
intents = Intents.default() intents = Intents.default()
intents.members = True intents.members = True
bot = Bot(command_prefix='Rafael', bot = Bot(command_prefix='Rafael',
@ -178,7 +195,6 @@ class PlayerProcess(Process):
# Add the Cogs for this bot too # Add the Cogs for this bot too
for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'): for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'):
print(filename)
if filename.endswith('.py'): if filename.endswith('.py'):
bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}') bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}')
@ -187,10 +203,7 @@ class PlayerProcess(Process):
await task await task
self.__loop.create_task(bot.connect(reconnect=True)) self.__loop.create_task(bot.connect(reconnect=True))
# Sleep to wait connection to be established # Sleep to wait connection to be established
await asyncio.sleep(1) await asyncio.sleep(2)
self.__guild: Guild = bot.get_guild(651983781258985484)
self.__voiceChannel = self.__bot.get_channel(933437427350118450)
return bot return bot
@ -228,7 +241,7 @@ class PlayerProcess(Process):
except: except:
return False return False
async def __connect(self) -> bool: async def __connectToVoiceChannel(self) -> bool:
try: try:
await self.__voiceChannel.connect(reconnect=True, timeout=None) await self.__voiceChannel.connect(reconnect=True, timeout=None)
return True return True

View File

@ -24,17 +24,34 @@ class ProcessManager(Singleton):
def getPlayerContext(self, guild: Guild, context: Context) -> ProcessContext: def getPlayerContext(self, guild: Guild, context: Context) -> ProcessContext:
try: try:
print('Get')
if guild not in self.__playersProcess.keys(): if guild not in self.__playersProcess.keys():
playlist: Playlist = self.__manager.Playlist() self.__playersProcess[guild] = self.__createProcess(context)
lock = Lock() else:
queue = Queue() if not self.__playersProcess[guild].getProcess().is_alive():
process = PlayerProcess(playlist, lock, queue) self.__playersProcess[guild] = self.__createProcess(context)
processContext = ProcessContext(process, queue, playlist, lock)
self.__playersProcess[guild] = processContext
return self.__playersProcess[guild] return self.__playersProcess[guild]
except Exception as e: except Exception as e:
print(e) print(f'[Error In GetPlayerContext] -> {e}')
def getRunningPlayerContext(self, guild: Guild) -> ProcessContext:
if guild not in self.__playersProcess.keys():
return None
return self.__playersProcess[guild]
def __createProcess(self, context: Context):
guildID: int = context.guild.id
textID: int = context.channel.id
voiceID: int = context.author.voice.channel.id
authorID: int = context.author.id
playlist: Playlist = self.__manager.Playlist()
lock = Lock()
queue = Queue()
process = PlayerProcess(playlist, lock, queue, guildID, textID, voiceID, authorID)
processContext = ProcessContext(process, queue, playlist, lock)
return processContext
class Manager(BaseManager): class Manager(BaseManager):