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:
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,
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
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:
|
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.")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue