From a9967918ffc47408e237cf7fc99ab147a978596d Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 31 Jul 2019 16:18:19 +0200 Subject: [PATCH] rework is finally finished --- royalnet/audio/__init__.py | 11 ++++++----- royalnet/audio/fileaudiosource.py | 4 ++-- royalnet/audio/playmodes.py | 5 +++-- royalnet/audio/ytdldiscord.py | 32 +++++++++++++++++++++++++++++-- royalnet/bots/discord.py | 26 ++++++++++++------------- royalnet/commands/play.py | 20 ++++++++++++------- royalnet/commands/playmode.py | 8 +++----- royalnet/commands/queue.py | 4 ++-- royalnet/commands/videoinfo.py | 2 +- 9 files changed, 72 insertions(+), 40 deletions(-) diff --git a/royalnet/audio/__init__.py b/royalnet/audio/__init__.py index 0fbf4e24..ff7942f5 100644 --- a/royalnet/audio/__init__.py +++ b/royalnet/audio/__init__.py @@ -1,8 +1,9 @@ """Video and audio downloading related classes, mainly used for Discord voice bots.""" -from .playmodes import PlayMode, Playlist, Pool -from .ytdlfile import YtdlFile, YtdlInfo -from .royalpcmfile import RoyalPCMFile -from .royalpcmaudio import RoyalPCMAudio +from . import playmodes +from .ytdlinfo import YtdlInfo +from .ytdlfile import YtdlFile +from .fileaudiosource import FileAudioSource +from .ytdldiscord import YtdlDiscord -__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo", "RoyalPCMFile", "RoyalPCMAudio"] +__all__ = ["playmodes", "YtdlInfo", "YtdlFile", "FileAudioSource", "YtdlDiscord"] diff --git a/royalnet/audio/fileaudiosource.py b/royalnet/audio/fileaudiosource.py index 442b5885..110b6a53 100644 --- a/royalnet/audio/fileaudiosource.py +++ b/royalnet/audio/fileaudiosource.py @@ -7,7 +7,7 @@ class FileAudioSource(discord.AudioSource): The stream should be in the usual PCM encoding. Warning: - This AudioSource will consume (and close) the passed stream""" + This AudioSource will consume (and close) the passed stream.""" def __init__(self, file): self.file = file @@ -29,10 +29,10 @@ class FileAudioSource(discord.AudioSource): """Reads 20ms worth of audio. If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so.""" - data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) # If the stream is closed, it should stop playing immediatly if self.file.closed: return b"" + data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) # If there is no more data to be streamed if len(data) != discord.opus.Encoder.FRAME_SIZE: # Close the file diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py index 712fb45a..fad8a667 100644 --- a/royalnet/audio/playmodes.py +++ b/royalnet/audio/playmodes.py @@ -83,9 +83,10 @@ class Playlist(PlayMode): next_video = self.list.pop(0) except IndexError: self.now_playing = None + yield None else: self.now_playing = next_video - yield self.now_playing + yield next_video.spawn_audiosource() if self.now_playing is not None: self.now_playing.delete() @@ -130,7 +131,7 @@ class Pool(PlayMode): while self._pool_copy: next_video = self._pool_copy.pop(0) self.now_playing = next_video - yield next_video + yield next_video.spawn_audiosource() def add(self, item) -> None: self.pool.append(item) diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py index abf08f47..1b171b75 100644 --- a/royalnet/audio/ytdldiscord.py +++ b/royalnet/audio/ytdldiscord.py @@ -3,6 +3,7 @@ import discord import re import ffmpeg import os +from .ytdlinfo import YtdlInfo from .ytdlfile import YtdlFile from .fileaudiosource import FileAudioSource @@ -11,6 +12,7 @@ class YtdlDiscord: def __init__(self, ytdl_file: YtdlFile): self.ytdl_file: YtdlFile = ytdl_file self.pcm_filename: typing.Optional[str] = None + self._fas_spawned: typing.List[FileAudioSource] = [] def pcm_available(self): return self.pcm_filename is not None and os.path.exists(self.pcm_filename) @@ -35,14 +37,40 @@ class YtdlDiscord: if not self.pcm_available(): self.convert_to_pcm() - def to_audiosource(self) -> discord.AudioSource: + def spawn_audiosource(self) -> discord.AudioSource: if not self.pcm_available(): raise FileNotFoundError("File hasn't been converted to PCM yet") stream = open(self.pcm_filename, "rb") - return FileAudioSource(stream) + source = FileAudioSource(stream) + # FIXME: it's a intentional memory leak + self._fas_spawned.append(source) + return source def delete(self) -> None: if self.pcm_available(): + for source in self._fas_spawned: + if not source.file.closed: + source.file.close() os.remove(self.pcm_filename) self.pcm_filename = None self.ytdl_file.delete() + + @classmethod + def create_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: + files = YtdlFile.download_from_url(url, **ytdl_args) + dfiles = [] + for file in files: + dfile = YtdlDiscord(file) + dfiles.append(dfile) + return dfiles + + @classmethod + def create_and_ready_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: + dfiles = cls.create_from_url(url, **ytdl_args) + for dfile in dfiles: + dfile.ready_up() + return dfiles + + @property + def info(self) -> typing.Optional[YtdlInfo]: + return self.ytdl_file.info diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index b918a612..2c73b29d 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -8,7 +8,7 @@ from ..utils import asyncify, Call, Command, discord_escape from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError from ..database import DatabaseConfig -from ..audio import PlayMode, Playlist, RoyalPCMAudio +from ..audio import playmodes, YtdlDiscord log = _logging.getLogger(__name__) @@ -31,7 +31,7 @@ class DiscordBot(GenericBot): def _init_voice(self): """Initialize the variables needed for the connection to voice chat.""" log.debug(f"Creating music_data dict") - self.music_data: typing.Dict[discord.Guild, PlayMode] = {} + self.music_data: typing.Dict[discord.Guild, playmodes.PlayMode] = {} def _call_factory(self) -> typing.Type[Call]: log.debug(f"Creating DiscordCall") @@ -100,7 +100,7 @@ class DiscordBot(GenericBot): # Create a music_data entry, if it doesn't exist; default is a Playlist if not self.music_data.get(channel.guild): log.debug(f"Creating music_data for {channel.guild}") - self.music_data[channel.guild] = Playlist() + self.music_data[channel.guild] = playmodes.Playlist() @staticmethod async def on_message(message: discord.Message): @@ -216,12 +216,12 @@ class DiscordBot(GenericBot): await self.client.connect() # TODO: how to stop? - async def add_to_music_data(self, audio_sources: typing.List[RoyalPCMAudio], guild: discord.Guild): - """Add a file to the corresponding music_data object.""" + async def add_to_music_data(self, dfiles: typing.List[YtdlDiscord], guild: discord.Guild): + """Add a list of :py:class:`royalnet.audio.YtdlDiscord` to the corresponding music_data object.""" guild_music_data = self.music_data[guild] - for audio_source in audio_sources: - log.debug(f"Adding {audio_source} to music_data") - guild_music_data.add(audio_source) + for dfile in dfiles: + log.debug(f"Adding {dfile} to music_data") + guild_music_data.add(dfile) if guild_music_data.now_playing is None: await self.advance_music_data(guild) @@ -229,7 +229,7 @@ class DiscordBot(GenericBot): """Try to play the next song, while it exists. Otherwise, just return.""" guild_music_data = self.music_data[guild] voice_client = self.client.find_voice_client_by_guild(guild) - next_source: RoyalPCMAudio = await guild_music_data.next() + next_source: discord.AudioSource = await guild_music_data.next() await self.update_activity_with_source_title() if next_source is None: log.debug(f"Ending playback chain") @@ -252,16 +252,14 @@ class DiscordBot(GenericBot): log.debug(f"Updating current Activity: setting to None, as multiple guilds are using the bot") await self.client.change_presence(status=discord.Status.online) return - # FIXME: PyCharm faulty inspection? - # noinspection PyUnresolvedReferences - play_mode: PlayMode = list(self.music_data.items())[0][1] + play_mode: playmodes.PlayMode = self.music_data[list(self.music_data)[0]] now_playing = play_mode.now_playing if now_playing is None: # No songs are playing now log.debug(f"Updating current Activity: setting to None, as nothing is currently being played") await self.client.change_presence(status=discord.Status.online) return - log.debug(f"Updating current Activity: listening to {now_playing.rpf.info.title}") - await self.client.change_presence(activity=discord.Activity(name=now_playing.rpf.info.title, + log.debug(f"Updating current Activity: listening to {now_playing.info.title}") + await self.client.change_presence(activity=discord.Activity(name=now_playing.info.title, type=discord.ActivityType.listening), status=discord.Status.online) diff --git a/royalnet/commands/play.py b/royalnet/commands/play.py index 80c68167..7e302d3d 100644 --- a/royalnet/commands/play.py +++ b/royalnet/commands/play.py @@ -6,11 +6,17 @@ import pickle from ..utils import Command, Call, NetworkHandler, asyncify from ..network import Request, ResponseSuccess from ..error import TooManyFoundError, NoneFoundError -from ..audio import RoyalPCMAudio +from ..audio import YtdlDiscord if typing.TYPE_CHECKING: from ..bots import DiscordBot +ytdl_args = { + "format": "bestaudio", + "outtmpl": f"./downloads/%(autonumber)s_%(title)s.%(ext)s" +} + + class PlayNH(NetworkHandler): message_type = "music_play" @@ -32,16 +38,16 @@ class PlayNH(NetworkHandler): raise Exception("No music_data for this guild") # Start downloading if data["url"].startswith("http://") or data["url"].startswith("https://"): - audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, data["url"]) + dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_and_ready_from_url, data["url"], **ytdl_args) else: - audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, data["url"]) - await bot.add_to_music_data(audio_sources, guild) + dfiles = await asyncify(YtdlDiscord.create_and_ready_from_url, f"ytsearch:{data['url']}", **ytdl_args) + await bot.add_to_music_data(dfiles, guild) # Create response dictionary response = { "videos": [{ - "title": source.rpf.info.title, - "discord_embed_pickle": str(pickle.dumps(source.rpf.info.to_discord_embed())) - } for source in audio_sources] + "title": dfile.info.title, + "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed())) + } for dfile in dfiles] } return ResponseSuccess(response) diff --git a/royalnet/commands/playmode.py b/royalnet/commands/playmode.py index 0dbef442..10fb84d7 100644 --- a/royalnet/commands/playmode.py +++ b/royalnet/commands/playmode.py @@ -1,9 +1,8 @@ import typing -import asyncio from ..utils import Command, Call, NetworkHandler from ..network import Request, ResponseSuccess -from ..error import NoneFoundError, TooManyFoundError, CurrentlyDisabledError -from ..audio import Playlist, Pool +from ..error import NoneFoundError, TooManyFoundError +from ..audio.playmodes import Playlist, Pool if typing.TYPE_CHECKING: from ..bots import DiscordBot @@ -30,8 +29,7 @@ class PlaymodeNH(NetworkHandler): if data["mode_name"] == "playlist": bot.music_data[guild] = Playlist() elif data["mode_name"] == "pool": - # bot.music_data[guild] = Pool() - raise CurrentlyDisabledError("Bug: https://github.com/royal-games/royalnet/issues/61") + bot.music_data[guild] = Pool() else: raise ValueError("No such PlayMode") return ResponseSuccess() diff --git a/royalnet/commands/queue.py b/royalnet/commands/queue.py index f4fb1359..b2557604 100644 --- a/royalnet/commands/queue.py +++ b/royalnet/commands/queue.py @@ -37,8 +37,8 @@ class QueueNH(NetworkHandler): "type": playmode.__class__.__name__, "queue": { - "strings": [str(element.rpf.info) for element in queue], - "pickled_embeds": str(pickle.dumps([element.rpf.info.to_discord_embed() for element in queue])) + "strings": [str(dfile.info) for dfile in queue], + "pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue])) } }) diff --git a/royalnet/commands/videoinfo.py b/royalnet/commands/videoinfo.py index f92ac2bd..37929d7d 100644 --- a/royalnet/commands/videoinfo.py +++ b/royalnet/commands/videoinfo.py @@ -12,7 +12,7 @@ class VideoinfoCommand(Command): @classmethod async def common(cls, call: Call): url = call.args[0] - info_list = await asyncify(YtdlInfo.create_from_url, url) + info_list = await asyncify(YtdlInfo.retrieve_for_url, url) for info in info_list: info_dict = info.__dict__ message = f"🔍 Dati di [b]{info}[/b]:\n"