1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 13:34:28 +00:00

Automatically delete files when playback is complete or mode is changed

This commit is contained in:
Steffo 2019-04-20 18:03:19 +02:00
parent 2e7ef6f008
commit 6c8a93a33e
11 changed files with 142 additions and 34 deletions

View file

@ -24,7 +24,7 @@ logging.getLogger("royalnet.bots.telegram").setLevel(logging.DEBUG)
commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand, commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand,
AuthorCommand, DiarioCommand, RageCommand, DateparserCommand, ReminderCommand, KvactiveCommand, KvCommand, AuthorCommand, DiarioCommand, RageCommand, DateparserCommand, ReminderCommand, KvactiveCommand, KvCommand,
KvrollCommand, VideoinfoCommand, SummonCommand, PlayCommand, SkipCommand] KvrollCommand, VideoinfoCommand, SummonCommand, PlayCommand, SkipCommand, PlaymodeCommand]
address, port = "localhost", 1234 address, port = "localhost", 1234

View file

@ -1,11 +1,13 @@
import math import math
import random import random
import typing
from .royalpcmaudio import RoyalPCMAudio
class PlayMode: class PlayMode:
def __init__(self): def __init__(self):
self.now_playing = None self.now_playing: typing.Optional[RoyalPCMAudio] = None
self.generator = self._generator() self.generator: typing.AsyncGenerator = self._generator()
async def next(self): async def next(self):
return await self.generator.__anext__() return await self.generator.__anext__()
@ -14,21 +16,27 @@ class PlayMode:
raise NotImplementedError() raise NotImplementedError()
async def _generator(self): async def _generator(self):
"""Get the next video from the list and advance it.""" """Get the next RPA from the list and advance it."""
raise NotImplementedError()
# This is needed to make the coroutine an async generator
# noinspection PyUnreachableCode
yield NotImplemented yield NotImplemented
def add(self, item): def add(self, item):
"""Add a new video to the PlayMode.""" """Add a new RPA to the PlayMode."""
raise NotImplementedError() raise NotImplementedError()
def delete(self):
"""Delete all RPAs contained inside this PlayMode."""
class Playlist(PlayMode): class Playlist(PlayMode):
"""A video list. Videos played are removed from the list.""" """A video list. RPAs played are removed from the list."""
def __init__(self, starting_list=None): def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None):
super().__init__() super().__init__()
if starting_list is None: if starting_list is None:
starting_list = [] starting_list = []
self.list = starting_list self.list: typing.List[RoyalPCMAudio] = starting_list
def videos_left(self): def videos_left(self):
return len(self.list) return len(self.list)
@ -42,26 +50,33 @@ class Playlist(PlayMode):
else: else:
self.now_playing = next_video self.now_playing = next_video
yield self.now_playing yield self.now_playing
if self.now_playing is not None:
self.now_playing.delete()
def add(self, item): def add(self, item):
self.list.append(item) self.list.append(item)
def delete(self):
while self.list:
self.list.pop(0).delete()
self.now_playing.delete()
class Pool(PlayMode): class Pool(PlayMode):
"""A video pool. Videos played are played back in random order, and they are kept in the pool.""" """A RPA pool. RPAs played are played back in random order, and they are kept in the pool."""
def __init__(self, starting_pool=None): def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None):
super().__init__() super().__init__()
if starting_pool is None: if starting_pool is None:
starting_pool = [] starting_pool = []
self.pool = starting_pool self.pool: typing.List[RoyalPCMAudio] = starting_pool
self._pool_copy = [] self._pool_copy: typing.List[RoyalPCMAudio] = []
def videos_left(self): def videos_left(self):
return math.inf return math.inf
async def _generator(self): async def _generator(self):
while True: while True:
if self.pool: if not self.pool:
self.now_playing = None self.now_playing = None
yield None yield None
continue continue
@ -74,5 +89,11 @@ class Pool(PlayMode):
def add(self, item): def add(self, item):
self.pool.append(item) self.pool.append(item)
self._pool_copy.append(self._pool_copy) self._pool_copy.append(item)
random.shuffle(self._pool_copy) random.shuffle(self._pool_copy)
def delete(self):
for item in self.pool:
item.delete()
self.pool = None
self._pool_copy = None

View file

@ -7,7 +7,7 @@ from .royalpcmfile import RoyalPCMFile
class RoyalPCMAudio(AudioSource): class RoyalPCMAudio(AudioSource):
def __init__(self, rpf: "RoyalPCMFile"): def __init__(self, rpf: "RoyalPCMFile"):
self.rpf: "RoyalPCMFile" = rpf self.rpf: "RoyalPCMFile" = rpf
self._file = open(rpf.audio_filename, "rb") self._file = open(self.rpf.audio_filename, "rb")
@staticmethod @staticmethod
def create_from_url(url) -> typing.List["RoyalPCMAudio"]: def create_from_url(url) -> typing.List["RoyalPCMAudio"]:
@ -19,13 +19,20 @@ class RoyalPCMAudio(AudioSource):
def read(self): def read(self):
data: bytes = self._file.read(OpusEncoder.FRAME_SIZE) data: bytes = self._file.read(OpusEncoder.FRAME_SIZE)
# If the file was externally closed, it means it was deleted
if self._file.closed:
return b""
if len(data) != OpusEncoder.FRAME_SIZE: if len(data) != OpusEncoder.FRAME_SIZE:
# Close the file as soon as the playback is finished
self._file.close()
# Reopen the file, so it can be reused
self._file = open(self.rpf.audio_filename, "rb")
return b"" return b""
return data return data
def delete(self):
self._file.close()
self.rpf.delete_audio_file()
def __repr__(self): def __repr__(self):
return f"<RoyalPCMAudio {self.rpf.audio_filename}>" return f"<RoyalPCMAudio {self.rpf.audio_filename}>"
def __del__(self):
self._file.close()
del self.rpf

View file

@ -28,7 +28,7 @@ class RoyalPCMFile(YtdlFile):
log.info(f"Now downloading {info.webpage_url}") log.info(f"Now downloading {info.webpage_url}")
super().__init__(info, outtmpl=self._ytdl_filename, **self.ytdl_args) super().__init__(info, outtmpl=self._ytdl_filename, **self.ytdl_args)
# Find the audio_filename with a regex (should be video.opus) # Find the audio_filename with a regex (should be video.opus)
log.info(f"Preparing {self.video_filename}...") log.info(f"Converting {self.video_filename}...")
# Convert the video to pcm # Convert the video to pcm
try: try:
ffmpeg.input(f"./{self.video_filename}") \ ffmpeg.input(f"./{self.video_filename}") \
@ -58,6 +58,6 @@ class RoyalPCMFile(YtdlFile):
def audio_filename(self): def audio_filename(self):
return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm" return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm"
def __del__(self): def delete_audio_file(self):
log.info(f"Deleting {self.audio_filename}") log.info(f"Deleting {self.audio_filename}")
os.remove(self.audio_filename) os.remove(self.audio_filename)

View file

@ -147,7 +147,3 @@ class YtdlInfo:
if self.webpage_url: if self.webpage_url:
return self.webpage_url return self.webpage_url
return self.id return self.id
if __name__ == "__main__":
f = YtdlFile.create_from_url("https://www.youtube.com/watch?v=BaW_jenozKc&v=UxxajLWwzqY", "./lovely.mp4")

View file

@ -102,7 +102,7 @@ class DiscordBot(GenericBot):
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] = Playlist()
@staticmethod # Not really static because of the self reference @staticmethod
async def on_message(message: discord.Message): async def on_message(message: discord.Message):
text = message.content text = message.content
# Skip non-text messages # Skip non-text messages
@ -119,6 +119,10 @@ class DiscordBot(GenericBot):
# Call the command # Call the command
await self.call(command_name, message.channel, parameters, message=message) await self.call(command_name, message.channel, parameters, message=message)
async def on_ready(cli):
log.debug("Connection successful, client is ready")
await cli.change_presence(status=discord.Status.online)
def find_guild_by_name(cli, name: str) -> discord.Guild: def find_guild_by_name(cli, name: str) -> discord.Guild:
"""Find the Guild with the specified name. Case-insensitive. """Find the Guild with the specified name. Case-insensitive.
Will raise a NoneFoundError if no channels are found, or a TooManyFoundError if more than one is found.""" Will raise a NoneFoundError if no channels are found, or a TooManyFoundError if more than one is found."""
@ -212,10 +216,10 @@ class DiscordBot(GenericBot):
async def advance_music_data(self, guild: discord.Guild): async def advance_music_data(self, guild: discord.Guild):
"""Try to play the next song, while it exists. Otherwise, just return.""" """Try to play the next song, while it exists. Otherwise, just return."""
log.debug(f"Starting playback chain")
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: RoyalPCMAudio = await guild_music_data.next()
await self.update_activity_with_source_title(next_source)
if next_source is None: if next_source is None:
log.debug(f"Ending playback chain") log.debug(f"Ending playback chain")
return return
@ -227,3 +231,19 @@ class DiscordBot(GenericBot):
log.debug(f"Starting playback of {next_source}") log.debug(f"Starting playback of {next_source}")
voice_client.play(next_source, after=advance) voice_client.play(next_source, after=advance)
async def update_activity_with_source_title(self, rpa: typing.Optional[RoyalPCMAudio] = None):
if len(self.music_data) > 1:
# Multiple guilds are using the bot, do not display anything
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
if rpa 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 {rpa.rpf.info.title}")
await self.client.change_presence(activity=discord.Activity(name=rpa.rpf.info.title,
type=discord.ActivityType.listening),
status=discord.Status.online)

View file

@ -17,9 +17,10 @@ from .videoinfo import VideoinfoCommand
from .summon import SummonCommand from .summon import SummonCommand
from .play import PlayCommand from .play import PlayCommand
from .skip import SkipCommand from .skip import SkipCommand
from .playmode import PlaymodeCommand
__all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand", __all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand",
"SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand", "SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand",
"KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand", "KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand",
"SkipCommand"] "SkipCommand", "PlaymodeCommand"]

View file

@ -2,7 +2,7 @@ import typing
import asyncio import asyncio
from ..utils import Command, Call, NetworkHandler from ..utils import Command, Call, NetworkHandler
from ..network import Message, RequestSuccessful from ..network import Message, RequestSuccessful
from ..error import TooManyFoundError from ..error import TooManyFoundError, NoneFoundError
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..bots import DiscordBot from ..bots import DiscordBot
@ -26,7 +26,9 @@ class PlayNH(NetworkHandler):
if message.guild_name: if message.guild_name:
guild = bot.client.find_guild(message.guild_name) guild = bot.client.find_guild(message.guild_name)
else: else:
if len(bot.music_data) != 1: if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
if len(bot.music_data) > 1:
raise TooManyFoundError("Multiple guilds found") raise TooManyFoundError("Multiple guilds found")
guild = list(bot.music_data)[0] guild = list(bot.music_data)[0]
# Ensure the guild has a PlayMode before adding the file to it # Ensure the guild has a PlayMode before adding the file to it

View file

@ -0,0 +1,59 @@
import typing
import asyncio
from ..utils import Command, Call, NetworkHandler
from ..network import Message, RequestSuccessful
from ..error import NoneFoundError, TooManyFoundError
from ..audio import Playlist, Pool
if typing.TYPE_CHECKING:
from ..bots import DiscordBot
loop = asyncio.get_event_loop()
class PlaymodeMessage(Message):
def __init__(self, mode_name: str, guild_name: typing.Optional[str] = None):
self.mode_name: str = mode_name
self.guild_name: typing.Optional[str] = guild_name
class PlaymodeNH(NetworkHandler):
message_type = PlaymodeMessage
@classmethod
async def discord(cls, bot: "DiscordBot", message: PlaymodeMessage):
"""Handle a play Royalnet request. That is, add audio to a PlayMode."""
# Find the matching guild
if message.guild_name:
guild = bot.client.find_guild(message.guild_name)
else:
if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
if len(bot.music_data) > 1:
raise TooManyFoundError("Multiple guilds found")
guild = list(bot.music_data)[0]
# Delete the previous PlayMode, if it exists
if bot.music_data[guild] is not None:
bot.music_data[guild].delete()
# Create the new PlayMode
if message.mode_name == "playlist":
bot.music_data[guild] = Playlist()
elif message.mode_name == "pool":
bot.music_data[guild] = Pool()
else:
raise ValueError("No such PlayMode")
return RequestSuccessful()
class PlaymodeCommand(Command):
command_name = "playmode"
command_description = "Cambia modalità di riproduzione per la chat vocale."
command_syntax = "[ [guild] ] (mode)"
network_handlers = [PlaymodeNH]
@classmethod
async def common(cls, call: Call):
guild, mode_name = call.args.match(r"(?:\[(.+)])?\s*(\S+)\s*")
await call.net_request(PlaymodeMessage(mode_name, guild), "discord")
await call.reply(f"✅ Richiesto di passare alla modalità di riproduzione [c]{mode_name}[/c].")

View file

@ -21,7 +21,9 @@ class SkipNH(NetworkHandler):
if message.guild_name: if message.guild_name:
guild = bot.client.find_guild_by_name(message.guild_name) guild = bot.client.find_guild_by_name(message.guild_name)
else: else:
if len(bot.music_data) != 1: if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
if len(bot.music_data) > 1:
raise TooManyFoundError("Multiple guilds found") raise TooManyFoundError("Multiple guilds found")
guild = list(bot.music_data)[0] guild = list(bot.music_data)[0]
# Set the currently playing source as ended # Set the currently playing source as ended
@ -45,4 +47,4 @@ class SkipCommand(Command):
async def common(cls, call: Call): async def common(cls, call: Call):
guild, = call.args.match(r"(?:\[(.+)])?") guild, = call.args.match(r"(?:\[(.+)])?")
await call.net_request(SkipMessage(guild), "discord") await call.net_request(SkipMessage(guild), "discord")
await call.reply(f"✅ Richiesta lo skip della canzone attuale..") await call.reply(f"✅ Richiesto lo skip della canzone attuale.")

View file

@ -25,8 +25,8 @@ class InvalidConfigError(Exception):
class RoyalnetError(Exception): class RoyalnetError(Exception):
"""An error was raised while handling the Royalnet request. """An error was raised while handling the Royalnet request.
This exception contains the exception that was raised during the handling.""" This exception contains the exception that was raised during the handling."""
def __init__(self, exc): def __init__(self, exc: Exception):
self.exc = exc self.exc: Exception = exc
class ExternalError(Exception): class ExternalError(Exception):