1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

rework is finally finished

This commit is contained in:
Steffo 2019-07-31 16:18:19 +02:00
parent d570e09e90
commit a9967918ff
9 changed files with 72 additions and 40 deletions

View file

@ -1,8 +1,9 @@
"""Video and audio downloading related classes, mainly used for Discord voice bots.""" """Video and audio downloading related classes, mainly used for Discord voice bots."""
from .playmodes import PlayMode, Playlist, Pool from . import playmodes
from .ytdlfile import YtdlFile, YtdlInfo from .ytdlinfo import YtdlInfo
from .royalpcmfile import RoyalPCMFile from .ytdlfile import YtdlFile
from .royalpcmaudio import RoyalPCMAudio from .fileaudiosource import FileAudioSource
from .ytdldiscord import YtdlDiscord
__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo", "RoyalPCMFile", "RoyalPCMAudio"] __all__ = ["playmodes", "YtdlInfo", "YtdlFile", "FileAudioSource", "YtdlDiscord"]

View file

@ -7,7 +7,7 @@ class FileAudioSource(discord.AudioSource):
The stream should be in the usual PCM encoding. The stream should be in the usual PCM encoding.
Warning: Warning:
This AudioSource will consume (and close) the passed stream""" This AudioSource will consume (and close) the passed stream."""
def __init__(self, file): def __init__(self, file):
self.file = file self.file = file
@ -29,10 +29,10 @@ class FileAudioSource(discord.AudioSource):
"""Reads 20ms worth of audio. """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.""" 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 the stream is closed, it should stop playing immediatly
if self.file.closed: if self.file.closed:
return b"" return b""
data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE)
# If there is no more data to be streamed # If there is no more data to be streamed
if len(data) != discord.opus.Encoder.FRAME_SIZE: if len(data) != discord.opus.Encoder.FRAME_SIZE:
# Close the file # Close the file

View file

@ -83,9 +83,10 @@ class Playlist(PlayMode):
next_video = self.list.pop(0) next_video = self.list.pop(0)
except IndexError: except IndexError:
self.now_playing = None self.now_playing = None
yield None
else: else:
self.now_playing = next_video self.now_playing = next_video
yield self.now_playing yield next_video.spawn_audiosource()
if self.now_playing is not None: if self.now_playing is not None:
self.now_playing.delete() self.now_playing.delete()
@ -130,7 +131,7 @@ class Pool(PlayMode):
while self._pool_copy: while self._pool_copy:
next_video = self._pool_copy.pop(0) next_video = self._pool_copy.pop(0)
self.now_playing = next_video self.now_playing = next_video
yield next_video yield next_video.spawn_audiosource()
def add(self, item) -> None: def add(self, item) -> None:
self.pool.append(item) self.pool.append(item)

View file

@ -3,6 +3,7 @@ import discord
import re import re
import ffmpeg import ffmpeg
import os import os
from .ytdlinfo import YtdlInfo
from .ytdlfile import YtdlFile from .ytdlfile import YtdlFile
from .fileaudiosource import FileAudioSource from .fileaudiosource import FileAudioSource
@ -11,6 +12,7 @@ class YtdlDiscord:
def __init__(self, ytdl_file: YtdlFile): def __init__(self, ytdl_file: YtdlFile):
self.ytdl_file: YtdlFile = ytdl_file self.ytdl_file: YtdlFile = ytdl_file
self.pcm_filename: typing.Optional[str] = None self.pcm_filename: typing.Optional[str] = None
self._fas_spawned: typing.List[FileAudioSource] = []
def pcm_available(self): def pcm_available(self):
return self.pcm_filename is not None and os.path.exists(self.pcm_filename) 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(): if not self.pcm_available():
self.convert_to_pcm() self.convert_to_pcm()
def to_audiosource(self) -> discord.AudioSource: def spawn_audiosource(self) -> discord.AudioSource:
if not self.pcm_available(): if not self.pcm_available():
raise FileNotFoundError("File hasn't been converted to PCM yet") raise FileNotFoundError("File hasn't been converted to PCM yet")
stream = open(self.pcm_filename, "rb") 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: def delete(self) -> None:
if self.pcm_available(): if self.pcm_available():
for source in self._fas_spawned:
if not source.file.closed:
source.file.close()
os.remove(self.pcm_filename) os.remove(self.pcm_filename)
self.pcm_filename = None self.pcm_filename = None
self.ytdl_file.delete() 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

View file

@ -8,7 +8,7 @@ from ..utils import asyncify, Call, Command, discord_escape
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError
from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError
from ..database import DatabaseConfig from ..database import DatabaseConfig
from ..audio import PlayMode, Playlist, RoyalPCMAudio from ..audio import playmodes, YtdlDiscord
log = _logging.getLogger(__name__) log = _logging.getLogger(__name__)
@ -31,7 +31,7 @@ class DiscordBot(GenericBot):
def _init_voice(self): def _init_voice(self):
"""Initialize the variables needed for the connection to voice chat.""" """Initialize the variables needed for the connection to voice chat."""
log.debug(f"Creating music_data dict") 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]: def _call_factory(self) -> typing.Type[Call]:
log.debug(f"Creating DiscordCall") 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 # Create a music_data entry, if it doesn't exist; default is a Playlist
if not self.music_data.get(channel.guild): if not self.music_data.get(channel.guild):
log.debug(f"Creating music_data for {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 @staticmethod
async def on_message(message: discord.Message): async def on_message(message: discord.Message):
@ -216,12 +216,12 @@ class DiscordBot(GenericBot):
await self.client.connect() await self.client.connect()
# TODO: how to stop? # TODO: how to stop?
async def add_to_music_data(self, audio_sources: typing.List[RoyalPCMAudio], guild: discord.Guild): async def add_to_music_data(self, dfiles: typing.List[YtdlDiscord], guild: discord.Guild):
"""Add a file to the corresponding music_data object.""" """Add a list of :py:class:`royalnet.audio.YtdlDiscord` to the corresponding music_data object."""
guild_music_data = self.music_data[guild] guild_music_data = self.music_data[guild]
for audio_source in audio_sources: for dfile in dfiles:
log.debug(f"Adding {audio_source} to music_data") log.debug(f"Adding {dfile} to music_data")
guild_music_data.add(audio_source) guild_music_data.add(dfile)
if guild_music_data.now_playing is None: if guild_music_data.now_playing is None:
await self.advance_music_data(guild) 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.""" """Try to play the next song, while it exists. Otherwise, just return."""
guild_music_data = self.music_data[guild] guild_music_data = self.music_data[guild]
voice_client = self.client.find_voice_client_by_guild(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() await self.update_activity_with_source_title()
if next_source is None: if next_source is None:
log.debug(f"Ending playback chain") 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") 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) await self.client.change_presence(status=discord.Status.online)
return return
# FIXME: PyCharm faulty inspection? play_mode: playmodes.PlayMode = self.music_data[list(self.music_data)[0]]
# noinspection PyUnresolvedReferences
play_mode: PlayMode = list(self.music_data.items())[0][1]
now_playing = play_mode.now_playing now_playing = play_mode.now_playing
if now_playing is None: if now_playing is None:
# No songs are playing now # No songs are playing now
log.debug(f"Updating current Activity: setting to None, as nothing is currently being played") log.debug(f"Updating current Activity: setting to None, as nothing is currently being played")
await self.client.change_presence(status=discord.Status.online) await self.client.change_presence(status=discord.Status.online)
return return
log.debug(f"Updating current Activity: listening to {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.rpf.info.title, await self.client.change_presence(activity=discord.Activity(name=now_playing.info.title,
type=discord.ActivityType.listening), type=discord.ActivityType.listening),
status=discord.Status.online) status=discord.Status.online)

View file

@ -6,11 +6,17 @@ import pickle
from ..utils import Command, Call, NetworkHandler, asyncify from ..utils import Command, Call, NetworkHandler, asyncify
from ..network import Request, ResponseSuccess from ..network import Request, ResponseSuccess
from ..error import TooManyFoundError, NoneFoundError from ..error import TooManyFoundError, NoneFoundError
from ..audio import RoyalPCMAudio from ..audio import YtdlDiscord
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..bots import DiscordBot from ..bots import DiscordBot
ytdl_args = {
"format": "bestaudio",
"outtmpl": f"./downloads/%(autonumber)s_%(title)s.%(ext)s"
}
class PlayNH(NetworkHandler): class PlayNH(NetworkHandler):
message_type = "music_play" message_type = "music_play"
@ -32,16 +38,16 @@ class PlayNH(NetworkHandler):
raise Exception("No music_data for this guild") raise Exception("No music_data for this guild")
# Start downloading # Start downloading
if data["url"].startswith("http://") or data["url"].startswith("https://"): 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: else:
audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, data["url"]) dfiles = await asyncify(YtdlDiscord.create_and_ready_from_url, f"ytsearch:{data['url']}", **ytdl_args)
await bot.add_to_music_data(audio_sources, guild) await bot.add_to_music_data(dfiles, guild)
# Create response dictionary # Create response dictionary
response = { response = {
"videos": [{ "videos": [{
"title": source.rpf.info.title, "title": dfile.info.title,
"discord_embed_pickle": str(pickle.dumps(source.rpf.info.to_discord_embed())) "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
} for source in audio_sources] } for dfile in dfiles]
} }
return ResponseSuccess(response) return ResponseSuccess(response)

View file

@ -1,9 +1,8 @@
import typing import typing
import asyncio
from ..utils import Command, Call, NetworkHandler from ..utils import Command, Call, NetworkHandler
from ..network import Request, ResponseSuccess from ..network import Request, ResponseSuccess
from ..error import NoneFoundError, TooManyFoundError, CurrentlyDisabledError from ..error import NoneFoundError, TooManyFoundError
from ..audio import Playlist, Pool from ..audio.playmodes import Playlist, Pool
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..bots import DiscordBot from ..bots import DiscordBot
@ -30,8 +29,7 @@ class PlaymodeNH(NetworkHandler):
if data["mode_name"] == "playlist": if data["mode_name"] == "playlist":
bot.music_data[guild] = Playlist() bot.music_data[guild] = Playlist()
elif data["mode_name"] == "pool": elif data["mode_name"] == "pool":
# bot.music_data[guild] = Pool() bot.music_data[guild] = Pool()
raise CurrentlyDisabledError("Bug: https://github.com/royal-games/royalnet/issues/61")
else: else:
raise ValueError("No such PlayMode") raise ValueError("No such PlayMode")
return ResponseSuccess() return ResponseSuccess()

View file

@ -37,8 +37,8 @@ class QueueNH(NetworkHandler):
"type": playmode.__class__.__name__, "type": playmode.__class__.__name__,
"queue": "queue":
{ {
"strings": [str(element.rpf.info) for element in queue], "strings": [str(dfile.info) for dfile in queue],
"pickled_embeds": str(pickle.dumps([element.rpf.info.to_discord_embed() for element in queue])) "pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue]))
} }
}) })

View file

@ -12,7 +12,7 @@ class VideoinfoCommand(Command):
@classmethod @classmethod
async def common(cls, call: Call): async def common(cls, call: Call):
url = call.args[0] 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: for info in info_list:
info_dict = info.__dict__ info_dict = info.__dict__
message = f"🔍 Dati di [b]{info}[/b]:\n" message = f"🔍 Dati di [b]{info}[/b]:\n"