mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-05 17:02:32 -05:00
[Audio] New stuff from RLL 0.7.0 (#4529)
* New stuff from RLL 0.7.0 * discard here * formatting * do this properly * make it more unique * bump RLL * nuke `[p]llset restport`, only `[p]llset wsport` matters * Update setup.cfg * properly deprecate Rest port and Ensure Nodes are properly closed upon running LLSET commands * restore player on a attempt reconnect * restore player as a task * ensure we send the signal only if not playing. * register events a little earlier * hmmm * ffs * update application.yml * fix permissions edge case
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
@@ -62,6 +63,7 @@ class Audio(
|
|||||||
self.play_lock = {}
|
self.play_lock = {}
|
||||||
|
|
||||||
self.lavalink_connect_task = None
|
self.lavalink_connect_task = None
|
||||||
|
self._restore_task = None
|
||||||
self.player_automated_timer_task = None
|
self.player_automated_timer_task = None
|
||||||
self.cog_cleaned_up = False
|
self.cog_cleaned_up = False
|
||||||
self.lavalink_connection_aborted = False
|
self.lavalink_connection_aborted = False
|
||||||
@@ -82,6 +84,8 @@ class Audio(
|
|||||||
"can_post": False,
|
"can_post": False,
|
||||||
"can_delete": False,
|
"can_delete": False,
|
||||||
}
|
}
|
||||||
|
self._ll_guild_updates = set()
|
||||||
|
self._last_ll_update = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
default_global = dict(
|
default_global = dict(
|
||||||
schema_version=1,
|
schema_version=1,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple, Union
|
from typing import Set, TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
@@ -57,6 +58,7 @@ class MixinMeta(ABC):
|
|||||||
_error_counter: Counter
|
_error_counter: Counter
|
||||||
|
|
||||||
lavalink_connect_task: Optional[asyncio.Task]
|
lavalink_connect_task: Optional[asyncio.Task]
|
||||||
|
_restore_task: Optional[asyncio.Task]
|
||||||
player_automated_timer_task: Optional[asyncio.Task]
|
player_automated_timer_task: Optional[asyncio.Task]
|
||||||
cog_init_task: Optional[asyncio.Task]
|
cog_init_task: Optional[asyncio.Task]
|
||||||
cog_ready_event: asyncio.Event
|
cog_ready_event: asyncio.Event
|
||||||
@@ -64,6 +66,9 @@ class MixinMeta(ABC):
|
|||||||
_default_lavalink_settings: Mapping
|
_default_lavalink_settings: Mapping
|
||||||
permission_cache = discord.Permissions
|
permission_cache = discord.Permissions
|
||||||
|
|
||||||
|
_last_ll_update: datetime.datetime
|
||||||
|
_ll_guild_updates: Set[int]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def command_llsetup(self, ctx: commands.Context):
|
async def command_llsetup(self, ctx: commands.Context):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -122,6 +127,12 @@ class MixinMeta(ABC):
|
|||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def lavalink_update_handler(
|
||||||
|
self, player: lavalink.Player, event_type: lavalink.enums.PlayerState, extra
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def _clear_react(
|
async def _clear_react(
|
||||||
self, message: discord.Message, emoji: MutableMapping = None
|
self, message: discord.Message, emoji: MutableMapping = None
|
||||||
|
|||||||
@@ -1472,14 +1472,12 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
async def command_audioset_restart(self, ctx: commands.Context):
|
async def command_audioset_restart(self, ctx: commands.Context):
|
||||||
"""Restarts the lavalink connection."""
|
"""Restarts the lavalink connection."""
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
lavalink.unregister_event_listener(self.lavalink_event_handler)
|
|
||||||
await lavalink.close()
|
await lavalink.close()
|
||||||
if self.player_manager is not None:
|
if self.player_manager is not None:
|
||||||
await self.player_manager.shutdown()
|
await self.player_manager.shutdown()
|
||||||
|
|
||||||
self.lavalink_restart_connect()
|
self.lavalink_restart_connect()
|
||||||
lavalink.register_event_listener(self.lavalink_event_handler)
|
|
||||||
await self.restore_players()
|
|
||||||
await self.send_embed_msg(
|
await self.send_embed_msg(
|
||||||
ctx,
|
ctx,
|
||||||
title=_("Restarting Lavalink"),
|
title=_("Restarting Lavalink"),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class PlayerControllerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
await self.config.custom("EQUALIZER", ctx.guild.id).eq_bands.set(eq.bands)
|
||||||
await player.stop()
|
await player.stop()
|
||||||
await player.disconnect()
|
await player.disconnect()
|
||||||
|
self._ll_guild_updates.discard(ctx.guild.id)
|
||||||
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
|
await self.api_interface.persistent_queue_api.drop(ctx.guild.id)
|
||||||
|
|
||||||
@commands.command(name="now")
|
@commands.command(name="now")
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
ctx,
|
ctx,
|
||||||
title=_("Failed To Shutdown Lavalink"),
|
title=_("Failed To Shutdown Lavalink"),
|
||||||
description=_(
|
description=_(
|
||||||
"For it to take effect please reload " "Audio (`{prefix}reload audio`)."
|
"For it to take effect please reload Audio (`{prefix}reload audio`)."
|
||||||
).format(
|
).format(
|
||||||
prefix=ctx.prefix,
|
prefix=ctx.prefix,
|
||||||
),
|
),
|
||||||
@@ -188,31 +188,6 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@command_llsetup.command(name="restport")
|
|
||||||
async def command_llsetup_restport(self, ctx: commands.Context, rest_port: int):
|
|
||||||
"""Set the Lavalink REST server port."""
|
|
||||||
await self.config.rest_port.set(rest_port)
|
|
||||||
footer = None
|
|
||||||
if await self.update_external_status():
|
|
||||||
footer = _("External Lavalink server set to True.")
|
|
||||||
await self.send_embed_msg(
|
|
||||||
ctx,
|
|
||||||
title=_("Setting Changed"),
|
|
||||||
description=_("REST port set to {port}.").format(port=rest_port),
|
|
||||||
footer=footer,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.lavalink_restart_connect()
|
|
||||||
except ProcessLookupError:
|
|
||||||
await self.send_embed_msg(
|
|
||||||
ctx,
|
|
||||||
title=_("Failed To Shutdown Lavalink"),
|
|
||||||
description=_("Please reload Audio (`{prefix}reload audio`).").format(
|
|
||||||
prefix=ctx.prefix
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@command_llsetup.command(name="wsport")
|
@command_llsetup.command(name="wsport")
|
||||||
async def command_llsetup_wsport(self, ctx: commands.Context, ws_port: int):
|
async def command_llsetup_wsport(self, ctx: commands.Context, ws_port: int):
|
||||||
"""Set the Lavalink websocket server port."""
|
"""Set the Lavalink websocket server port."""
|
||||||
@@ -248,8 +223,9 @@ class LavalinkSetupCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
ws_port = configs["ws_port"]
|
ws_port = configs["ws_port"]
|
||||||
msg = "----" + _("Connection Settings") + "---- \n"
|
msg = "----" + _("Connection Settings") + "---- \n"
|
||||||
msg += _("Host: [{host}]\n").format(host=host)
|
msg += _("Host: [{host}]\n").format(host=host)
|
||||||
msg += _("Rest Port: [{port}]\n").format(port=rest_port)
|
|
||||||
msg += _("WS Port: [{port}]\n").format(port=ws_port)
|
msg += _("WS Port: [{port}]\n").format(port=ws_port)
|
||||||
|
if ws_port != rest_port:
|
||||||
|
msg += _("Rest Port: [{port}]\n").format(port=rest_port)
|
||||||
msg += _("Password: [{password}]\n").format(password=password)
|
msg += _("Password: [{password}]\n").format(password=password)
|
||||||
try:
|
try:
|
||||||
await self.send_embed_msg(ctx.author, description=box(msg, lang="ini"))
|
await self.send_embed_msg(ctx.author, description=box(msg, lang="ini"))
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ class PlayerCommands(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
if not await self.is_query_allowed(
|
if not await self.is_query_allowed(
|
||||||
self.config,
|
self.config,
|
||||||
ctx,
|
ctx,
|
||||||
f"{single_track.title} {single_track.author} {single_track.uri} " f"{str(query)}",
|
f"{single_track.title} {single_track.author} {single_track.uri} {str(query)}",
|
||||||
query_obj=query,
|
query_obj=query,
|
||||||
):
|
):
|
||||||
if IS_DEBUG:
|
if IS_DEBUG:
|
||||||
|
|||||||
@@ -239,7 +239,11 @@ class DpyEvents(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
if self.cog_init_task:
|
if self.cog_init_task:
|
||||||
self.cog_init_task.cancel()
|
self.cog_init_task.cancel()
|
||||||
|
|
||||||
|
if self._restore_task:
|
||||||
|
self._restore_task.cancel()
|
||||||
|
|
||||||
lavalink.unregister_event_listener(self.lavalink_event_handler)
|
lavalink.unregister_event_listener(self.lavalink_event_handler)
|
||||||
|
lavalink.unregister_update_listener(self.lavalink_update_handler)
|
||||||
self.bot.loop.create_task(lavalink.close())
|
self.bot.loop.create_task(lavalink.close())
|
||||||
if self.player_manager is not None:
|
if self.player_manager is not None:
|
||||||
self.bot.loop.create_task(self.player_manager.shutdown())
|
self.bot.loop.create_task(self.player_manager.shutdown())
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ _ = Translator("Audio", Path(__file__))
|
|||||||
|
|
||||||
|
|
||||||
class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
|
class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
|
||||||
|
async def lavalink_update_handler(
|
||||||
|
self, player: lavalink.Player, event_type: lavalink.enums.PlayerState, extra
|
||||||
|
):
|
||||||
|
self._last_ll_update = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
self._ll_guild_updates.add(int(extra.get("guildId", 0)))
|
||||||
|
|
||||||
async def lavalink_event_handler(
|
async def lavalink_event_handler(
|
||||||
self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra
|
self, player: lavalink.Player, event_type: lavalink.LavalinkEvents, extra
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -160,6 +167,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
if disconnect:
|
if disconnect:
|
||||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||||
await player.disconnect()
|
await player.disconnect()
|
||||||
|
self._ll_guild_updates.discard(guild.id)
|
||||||
if status:
|
if status:
|
||||||
player_check = await self.get_active_player_count()
|
player_check = await self.get_active_player_count()
|
||||||
await self.update_bot_presence(*player_check)
|
await self.update_bot_presence(*player_check)
|
||||||
@@ -193,6 +201,7 @@ class LavalinkEvents(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
await self.config.custom("EQUALIZER", guild_id).eq_bands.set(eq.bands)
|
await self.config.custom("EQUALIZER", guild_id).eq_bands.set(eq.bands)
|
||||||
await player.stop()
|
await player.stop()
|
||||||
await player.disconnect()
|
await player.disconnect()
|
||||||
|
self._ll_guild_updates.discard(guild_id)
|
||||||
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
self.bot.dispatch("red_audio_audio_disconnect", guild)
|
||||||
if message_channel:
|
if message_channel:
|
||||||
message_channel = self.bot.get_channel(message_channel)
|
message_channel = self.bot.get_channel(message_channel)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import lavalink
|
import lavalink
|
||||||
|
|
||||||
|
from redbot.core import data_manager
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
from ...errors import LavalinkDownloadFailed
|
from ...errors import LavalinkDownloadFailed
|
||||||
from ...manager import ServerManager
|
from ...manager import ServerManager
|
||||||
@@ -16,9 +17,16 @@ _ = Translator("Audio", Path(__file__))
|
|||||||
|
|
||||||
class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
||||||
def lavalink_restart_connect(self) -> None:
|
def lavalink_restart_connect(self) -> None:
|
||||||
|
lavalink.unregister_event_listener(self.lavalink_event_handler)
|
||||||
|
lavalink.unregister_update_listener(self.lavalink_update_handler)
|
||||||
if self.lavalink_connect_task:
|
if self.lavalink_connect_task:
|
||||||
self.lavalink_connect_task.cancel()
|
self.lavalink_connect_task.cancel()
|
||||||
|
if self._restore_task:
|
||||||
|
self._restore_task.cancel()
|
||||||
|
|
||||||
|
self._restore_task = None
|
||||||
|
lavalink.register_event_listener(self.lavalink_event_handler)
|
||||||
|
lavalink.register_update_listener(self.lavalink_update_handler)
|
||||||
self.lavalink_connect_task = self.bot.loop.create_task(self.lavalink_attempt_connect())
|
self.lavalink_connect_task = self.bot.loop.create_task(self.lavalink_attempt_connect())
|
||||||
|
|
||||||
async def lavalink_attempt_connect(self, timeout: int = 50) -> None:
|
async def lavalink_attempt_connect(self, timeout: int = 50) -> None:
|
||||||
@@ -33,7 +41,6 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
settings = self._default_lavalink_settings
|
settings = self._default_lavalink_settings
|
||||||
host = settings["host"]
|
host = settings["host"]
|
||||||
password = settings["password"]
|
password = settings["password"]
|
||||||
rest_port = settings["rest_port"]
|
|
||||||
ws_port = settings["ws_port"]
|
ws_port = settings["ws_port"]
|
||||||
if self.player_manager is not None:
|
if self.player_manager is not None:
|
||||||
await self.player_manager.shutdown()
|
await self.player_manager.shutdown()
|
||||||
@@ -73,7 +80,6 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
else:
|
else:
|
||||||
host = configs["host"]
|
host = configs["host"]
|
||||||
password = configs["password"]
|
password = configs["password"]
|
||||||
rest_port = configs["rest_port"]
|
|
||||||
ws_port = configs["ws_port"]
|
ws_port = configs["ws_port"]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -86,14 +92,17 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
|
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
while retry_count < max_retries:
|
while retry_count < max_retries:
|
||||||
|
if lavalink.node._nodes:
|
||||||
|
await lavalink.node.disconnect()
|
||||||
try:
|
try:
|
||||||
await lavalink.initialize(
|
await lavalink.initialize(
|
||||||
bot=self.bot,
|
bot=self.bot,
|
||||||
host=host,
|
host=host,
|
||||||
password=password,
|
password=password,
|
||||||
rest_port=rest_port,
|
rest_port=ws_port,
|
||||||
ws_port=ws_port,
|
ws_port=ws_port,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
resume_key=f"Red-Core-Audio-{self.bot.user.id}-{data_manager.instance_name}",
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
log.error("Connecting to Lavalink server timed out, retrying...")
|
log.error("Connecting to Lavalink server timed out, retrying...")
|
||||||
@@ -115,3 +124,5 @@ class LavalinkTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
"Connecting to the Lavalink server failed after multiple attempts. "
|
"Connecting to the Lavalink server failed after multiple attempts. "
|
||||||
"See above tracebacks for details."
|
"See above tracebacks for details."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
self._restore_task = asyncio.create_task(self.restore_players())
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
self.player_automated_timer()
|
self.player_automated_timer()
|
||||||
)
|
)
|
||||||
self.player_automated_timer_task.add_done_callback(task_callback)
|
self.player_automated_timer_task.add_done_callback(task_callback)
|
||||||
lavalink.register_event_listener(self.lavalink_event_handler)
|
|
||||||
await self.restore_players()
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.exception("Audio failed to start up, please report this issue.", exc_info=err)
|
log.exception("Audio failed to start up, please report this issue.", exc_info=err)
|
||||||
raise err
|
raise err
|
||||||
@@ -68,6 +66,7 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
async def restore_players(self):
|
async def restore_players(self):
|
||||||
tries = 0
|
tries = 0
|
||||||
tracks_to_restore = await self.api_interface.persistent_queue_api.fetch_all()
|
tracks_to_restore = await self.api_interface.persistent_queue_api.fetch_all()
|
||||||
|
await asyncio.sleep(10)
|
||||||
for guild_id, track_data in itertools.groupby(tracks_to_restore, key=lambda x: x.guild_id):
|
for guild_id, track_data in itertools.groupby(tracks_to_restore, key=lambda x: x.guild_id):
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
try:
|
try:
|
||||||
@@ -95,6 +94,12 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
while tries < 25 and vc is not None:
|
while tries < 25 and vc is not None:
|
||||||
try:
|
try:
|
||||||
vc = guild.get_channel(track_data[-1].room_id)
|
vc = guild.get_channel(track_data[-1].room_id)
|
||||||
|
if not vc:
|
||||||
|
break
|
||||||
|
perms = vc.permissions_for(guild.me)
|
||||||
|
if not (perms.connect and perms.speak):
|
||||||
|
vc = None
|
||||||
|
break
|
||||||
await lavalink.connect(vc)
|
await lavalink.connect(vc)
|
||||||
player = lavalink.get_player(guild.id)
|
player = lavalink.get_player(guild.id)
|
||||||
player.store("connect", datetime.datetime.utcnow())
|
player.store("connect", datetime.datetime.utcnow())
|
||||||
@@ -126,8 +131,8 @@ class StartUpTasks(MixinMeta, metaclass=CompositeMetaClass):
|
|||||||
track = track.track_object
|
track = track.track_object
|
||||||
player.add(guild.get_member(track.extras.get("requester")) or guild.me, track)
|
player.add(guild.get_member(track.extras.get("requester")) or guild.me, track)
|
||||||
player.maybe_shuffle()
|
player.maybe_shuffle()
|
||||||
|
if not player.is_playing:
|
||||||
await player.play()
|
await player.play()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
debug_exc_log(log, err, f"Error restoring player in {guild_id}")
|
debug_exc_log(log, err, f"Error restoring player in {guild_id}")
|
||||||
await self.api_interface.persistent_queue_api.drop(guild_id)
|
await self.api_interface.persistent_queue_api.drop(guild_id)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
server:
|
server:
|
||||||
host: "localhost"
|
host: "localhost"
|
||||||
port: 2333 # REST server
|
port: 2333 # WS port
|
||||||
lavalink:
|
lavalink:
|
||||||
server:
|
server:
|
||||||
password: "youshallnotpass"
|
password: "youshallnotpass"
|
||||||
@@ -18,7 +18,7 @@ lavalink:
|
|||||||
youtubePlaylistLoadLimit: 10000
|
youtubePlaylistLoadLimit: 10000
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
max-history: 30
|
max-history: 7
|
||||||
max-size: 1GB
|
max-size: 1GB
|
||||||
path: ./logs/
|
path: ./logs/
|
||||||
level:
|
level:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ install_requires =
|
|||||||
python-Levenshtein-wheels==0.13.1
|
python-Levenshtein-wheels==0.13.1
|
||||||
pytz==2020.1
|
pytz==2020.1
|
||||||
PyYAML==5.3.1
|
PyYAML==5.3.1
|
||||||
Red-Lavalink==0.6.0
|
Red-Lavalink==0.7.1
|
||||||
schema==0.7.2
|
schema==0.7.2
|
||||||
tqdm==4.48.0
|
tqdm==4.48.0
|
||||||
typing-extensions==3.7.4.2
|
typing-extensions==3.7.4.2
|
||||||
|
|||||||
Reference in New Issue
Block a user