mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
Automatically delete files when playback is complete or mode is changed
This commit is contained in:
parent
2e7ef6f008
commit
6c8a93a33e
11 changed files with 142 additions and 34 deletions
|
@ -24,7 +24,7 @@ logging.getLogger("royalnet.bots.telegram").setLevel(logging.DEBUG)
|
|||
|
||||
commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand,
|
||||
AuthorCommand, DiarioCommand, RageCommand, DateparserCommand, ReminderCommand, KvactiveCommand, KvCommand,
|
||||
KvrollCommand, VideoinfoCommand, SummonCommand, PlayCommand, SkipCommand]
|
||||
KvrollCommand, VideoinfoCommand, SummonCommand, PlayCommand, SkipCommand, PlaymodeCommand]
|
||||
|
||||
address, port = "localhost", 1234
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import math
|
||||
import random
|
||||
import typing
|
||||
from .royalpcmaudio import RoyalPCMAudio
|
||||
|
||||
|
||||
class PlayMode:
|
||||
def __init__(self):
|
||||
self.now_playing = None
|
||||
self.generator = self._generator()
|
||||
self.now_playing: typing.Optional[RoyalPCMAudio] = None
|
||||
self.generator: typing.AsyncGenerator = self._generator()
|
||||
|
||||
async def next(self):
|
||||
return await self.generator.__anext__()
|
||||
|
@ -14,21 +16,27 @@ class PlayMode:
|
|||
raise NotImplementedError()
|
||||
|
||||
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
|
||||
|
||||
def add(self, item):
|
||||
"""Add a new video to the PlayMode."""
|
||||
"""Add a new RPA to the PlayMode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete(self):
|
||||
"""Delete all RPAs contained inside this PlayMode."""
|
||||
|
||||
|
||||
class Playlist(PlayMode):
|
||||
"""A video list. Videos played are removed from the list."""
|
||||
def __init__(self, starting_list=None):
|
||||
"""A video list. RPAs played are removed from the list."""
|
||||
def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None):
|
||||
super().__init__()
|
||||
if starting_list is None:
|
||||
starting_list = []
|
||||
self.list = starting_list
|
||||
self.list: typing.List[RoyalPCMAudio] = starting_list
|
||||
|
||||
def videos_left(self):
|
||||
return len(self.list)
|
||||
|
@ -42,26 +50,33 @@ class Playlist(PlayMode):
|
|||
else:
|
||||
self.now_playing = next_video
|
||||
yield self.now_playing
|
||||
if self.now_playing is not None:
|
||||
self.now_playing.delete()
|
||||
|
||||
def add(self, item):
|
||||
self.list.append(item)
|
||||
|
||||
def delete(self):
|
||||
while self.list:
|
||||
self.list.pop(0).delete()
|
||||
self.now_playing.delete()
|
||||
|
||||
|
||||
class Pool(PlayMode):
|
||||
"""A video pool. Videos played are played back in random order, and they are kept in the pool."""
|
||||
def __init__(self, starting_pool=None):
|
||||
"""A RPA pool. RPAs played are played back in random order, and they are kept in the pool."""
|
||||
def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None):
|
||||
super().__init__()
|
||||
if starting_pool is None:
|
||||
starting_pool = []
|
||||
self.pool = starting_pool
|
||||
self._pool_copy = []
|
||||
self.pool: typing.List[RoyalPCMAudio] = starting_pool
|
||||
self._pool_copy: typing.List[RoyalPCMAudio] = []
|
||||
|
||||
def videos_left(self):
|
||||
return math.inf
|
||||
|
||||
async def _generator(self):
|
||||
while True:
|
||||
if self.pool:
|
||||
if not self.pool:
|
||||
self.now_playing = None
|
||||
yield None
|
||||
continue
|
||||
|
@ -74,5 +89,11 @@ class Pool(PlayMode):
|
|||
|
||||
def add(self, item):
|
||||
self.pool.append(item)
|
||||
self._pool_copy.append(self._pool_copy)
|
||||
self._pool_copy.append(item)
|
||||
random.shuffle(self._pool_copy)
|
||||
|
||||
def delete(self):
|
||||
for item in self.pool:
|
||||
item.delete()
|
||||
self.pool = None
|
||||
self._pool_copy = None
|
||||
|
|
|
@ -7,7 +7,7 @@ from .royalpcmfile import RoyalPCMFile
|
|||
class RoyalPCMAudio(AudioSource):
|
||||
def __init__(self, rpf: "RoyalPCMFile"):
|
||||
self.rpf: "RoyalPCMFile" = rpf
|
||||
self._file = open(rpf.audio_filename, "rb")
|
||||
self._file = open(self.rpf.audio_filename, "rb")
|
||||
|
||||
@staticmethod
|
||||
def create_from_url(url) -> typing.List["RoyalPCMAudio"]:
|
||||
|
@ -19,13 +19,20 @@ class RoyalPCMAudio(AudioSource):
|
|||
|
||||
def read(self):
|
||||
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:
|
||||
# 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 data
|
||||
|
||||
def delete(self):
|
||||
self._file.close()
|
||||
self.rpf.delete_audio_file()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RoyalPCMAudio {self.rpf.audio_filename}>"
|
||||
|
||||
def __del__(self):
|
||||
self._file.close()
|
||||
del self.rpf
|
||||
|
|
|
@ -28,7 +28,7 @@ class RoyalPCMFile(YtdlFile):
|
|||
log.info(f"Now downloading {info.webpage_url}")
|
||||
super().__init__(info, outtmpl=self._ytdl_filename, **self.ytdl_args)
|
||||
# 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
|
||||
try:
|
||||
ffmpeg.input(f"./{self.video_filename}") \
|
||||
|
@ -58,6 +58,6 @@ class RoyalPCMFile(YtdlFile):
|
|||
def audio_filename(self):
|
||||
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}")
|
||||
os.remove(self.audio_filename)
|
||||
|
|
|
@ -147,7 +147,3 @@ class YtdlInfo:
|
|||
if self.webpage_url:
|
||||
return self.webpage_url
|
||||
return self.id
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
f = YtdlFile.create_from_url("https://www.youtube.com/watch?v=BaW_jenozKc&v=UxxajLWwzqY", "./lovely.mp4")
|
||||
|
|
|
@ -102,7 +102,7 @@ class DiscordBot(GenericBot):
|
|||
log.debug(f"Creating music_data for {channel.guild}")
|
||||
self.music_data[channel.guild] = Playlist()
|
||||
|
||||
@staticmethod # Not really static because of the self reference
|
||||
@staticmethod
|
||||
async def on_message(message: discord.Message):
|
||||
text = message.content
|
||||
# Skip non-text messages
|
||||
|
@ -119,6 +119,10 @@ class DiscordBot(GenericBot):
|
|||
# Call the command
|
||||
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:
|
||||
"""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."""
|
||||
|
@ -212,10 +216,10 @@ class DiscordBot(GenericBot):
|
|||
|
||||
async def advance_music_data(self, guild: discord.Guild):
|
||||
"""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]
|
||||
voice_client = self.client.find_voice_client_by_guild(guild)
|
||||
next_source: RoyalPCMAudio = await guild_music_data.next()
|
||||
await self.update_activity_with_source_title(next_source)
|
||||
if next_source is None:
|
||||
log.debug(f"Ending playback chain")
|
||||
return
|
||||
|
@ -227,3 +231,19 @@ class DiscordBot(GenericBot):
|
|||
|
||||
log.debug(f"Starting playback of {next_source}")
|
||||
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)
|
||||
|
|
|
@ -17,9 +17,10 @@ from .videoinfo import VideoinfoCommand
|
|||
from .summon import SummonCommand
|
||||
from .play import PlayCommand
|
||||
from .skip import SkipCommand
|
||||
from .playmode import PlaymodeCommand
|
||||
|
||||
|
||||
__all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand",
|
||||
"SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand",
|
||||
"KvactiveCommand", "KvCommand", "KvrollCommand", "VideoinfoCommand", "SummonCommand", "PlayCommand",
|
||||
"SkipCommand"]
|
||||
"SkipCommand", "PlaymodeCommand"]
|
||||
|
|
|
@ -2,7 +2,7 @@ import typing
|
|||
import asyncio
|
||||
from ..utils import Command, Call, NetworkHandler
|
||||
from ..network import Message, RequestSuccessful
|
||||
from ..error import TooManyFoundError
|
||||
from ..error import TooManyFoundError, NoneFoundError
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..bots import DiscordBot
|
||||
|
||||
|
@ -26,7 +26,9 @@ class PlayNH(NetworkHandler):
|
|||
if message.guild_name:
|
||||
guild = bot.client.find_guild(message.guild_name)
|
||||
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")
|
||||
guild = list(bot.music_data)[0]
|
||||
# Ensure the guild has a PlayMode before adding the file to it
|
||||
|
|
59
royalnet/commands/playmode.py
Normal file
59
royalnet/commands/playmode.py
Normal 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].")
|
|
@ -21,7 +21,9 @@ class SkipNH(NetworkHandler):
|
|||
if message.guild_name:
|
||||
guild = bot.client.find_guild_by_name(message.guild_name)
|
||||
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")
|
||||
guild = list(bot.music_data)[0]
|
||||
# Set the currently playing source as ended
|
||||
|
@ -45,4 +47,4 @@ class SkipCommand(Command):
|
|||
async def common(cls, call: Call):
|
||||
guild, = call.args.match(r"(?:\[(.+)])?")
|
||||
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.")
|
||||
|
|
|
@ -25,8 +25,8 @@ class InvalidConfigError(Exception):
|
|||
class RoyalnetError(Exception):
|
||||
"""An error was raised while handling the Royalnet request.
|
||||
This exception contains the exception that was raised during the handling."""
|
||||
def __init__(self, exc):
|
||||
self.exc = exc
|
||||
def __init__(self, exc: Exception):
|
||||
self.exc: Exception = exc
|
||||
|
||||
|
||||
class ExternalError(Exception):
|
||||
|
|
Loading…
Reference in a new issue