42 Commits
Author SHA1 Message Date
matthew bd0b04bf28 Merge pull request #44 from strNophix/lastfm-scrobbler
Plugins + lastfm scrobbler
2021-12-08 19:39:01 +01:00
niku 935383e7c5 Merge pull request #43 from strNophix/ensure-queue-size
fill_player_queue now recursively refetches until queue size is ensured
2021-12-07 06:41:16 +01:00
niku fdc9dc16ed Readjusted slash_command bot kwarg 2021-12-06 21:02:10 +01:00
niku 0dc2e0a7ca Added plugins key to the sample config 2021-12-06 20:22:40 +01:00
niku 2a24a4b5f0 Commit hoarding is a terrible practice 2021-12-06 20:14:24 +01:00
niku 0a14d9a93c fill_player_queue now recursively refetches until queue size is ensured 2021-12-03 21:24:57 +01:00
matthew 76bfd719c3 Merge pull request #40 from strNophix/source-manager-setting
Source manager decorator
2021-11-22 21:10:12 +01:00
niku c45bd8ced3 Added decorator for checking if eligable for managing sources 2021-11-22 18:28:48 +01:00
niku b37c3f533a Merge pull request #39 from strNophix/remove-duplicate-commands
Removed duplicate commands
2021-11-22 11:15:55 +01:00
niku 9c55bcdc36 Removed duplicate commands 2021-11-22 11:11:17 +01:00
niku 1495d214a1 Merge pull request #37 from strNophix/database-rewrite
database rewrite + autojoin
2021-11-22 10:53:23 +01:00
niku 4f65522bd8 Merge branch 'dev' into database-rewrite 2021-11-22 10:53:00 +01:00
niku 3e6ee9ecc9 Merge pull request #38 from strNophix/decoupling-lavalink
decoupled lavalink creation from cogs.music
2021-11-21 20:54:52 +01:00
niku 8504d6abfd decoupled lavalink creation from cogs.music 2021-11-20 00:37:17 +01:00
niku d2ecbbf6ae prevent circular import in tunebot.redis.__init__.py 2021-11-19 21:58:47 +01:00
niku adb3fe11a6 Merge remote-tracking branch 'upstream/dev' into database-rewrite 2021-11-19 21:39:10 +01:00
niku ebb07960bf database rewrite 2021-11-19 21:10:29 +01:00
matthew 1697002666 Merge pull request #35 from strNophix/source-management
Source management
2021-11-19 19:34:24 +01:00
niku 60f0bd9070 Made source commandgroup hidden 2021-11-18 23:03:19 +01:00
niku b8dc8b2b20 Added owner checks for source command 2021-11-18 23:01:04 +01:00
niku 998749f1ef Merge pull request #33 from strNophix/owner-id-fix
Critical bug with unsupplied owner_ids
2021-11-18 22:21:28 +01:00
niku 12eb3a70e1 Critical fix with owner_id 2021-11-18 22:17:12 +01:00
niku 316f3bacc9 Added sources functionality + pre-commit 2021-11-18 21:59:37 +01:00
matthew 2e007a6fb3 Merge pull request #28 from Matthww/fix_wlinfo
Hot fix for wlinfo
2021-11-06 22:28:39 +01:00
matthew 47d64c2cb3 Temp fix for wlinfo 2021-11-06 22:28:04 +01:00
niku 162abc8617 Merge pull request #27 from Matthww/fix_wlinfo
Fixed wlinfo
2021-11-06 21:40:06 +01:00
matthew a3d9d71af4 Merge branch 'dev' into fix_wlinfo 2021-11-06 21:38:29 +01:00
matthew bc94157a0c Fixed wlinfo 2021-11-06 21:36:24 +01:00
niku a8db9e70df Merge pull request #24 from Matthww/add_packackages
Add packages
2021-11-06 20:48:09 +01:00
niku f6d52555dd Merge branch 'dev' into add_packackages 2021-11-06 20:47:52 +01:00
matthew 6658c96615 oops no hardcoding 2021-11-06 20:42:54 +01:00
matthew 707f908ebf Add packages to poetry 2021-11-06 20:40:48 +01:00
matthew af4a16669f Merge pull request #23 from strNophix/context-properties
using @property for existing context functions
2021-11-06 18:39:24 +01:00
niku 9b2babf4e3 using @property for existing context functions 2021-11-06 17:56:17 +01:00
Matthww c3fc008f3a pepega lock 2021-11-06 16:39:59 +00:00
niku 896bb93e69 Merge pull request #22 from Matthww/scripts
Add redis pumper 🍆
2021-11-06 17:26:19 +01:00
niku ae762683ab Merge branch 'dev' into scripts 2021-11-06 17:26:11 +01:00
matthew bb7062397e Add redis pumper 🍆 2021-11-06 17:21:59 +01:00
matthew 247c895136 Merge pull request #20 from strNophix/rebranding
Rebranding to TuneBot + updated pyproject.toml
2021-11-06 17:07:47 +01:00
matthew 7e74c1f5a7 Merge pull request #19 from strNophix/config-argument
Take bot config as argument
2021-11-06 17:07:20 +01:00
niku df90ba3a00 take bot config as argument 2021-11-06 16:08:19 +01:00
niku aed057daa4 Rebranding to TuneBot + updated pyproject.toml 2021-11-06 15:58:10 +01:00
36 changed files with 1363 additions and 207 deletions
+3
View File
@@ -1,3 +1,6 @@
# Project files
scripts/playlists/*.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+1 -1
View File
@@ -1,5 +1,5 @@
default_language_version:
python: python3.8
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
+2 -2
View File
@@ -1,2 +1,2 @@
# ChristmasBot
Christmas Music Bot
# TuneBot
A very configurable Discord radio
+85 -15
View File
@@ -4,6 +4,7 @@ from typing import Any
from typing import Dict
from typing import List
from typing import Sequence
from typing import TYPE_CHECKING
import aioredis
import discord
@@ -21,28 +22,74 @@ from discord.ext.commands.errors import ExtensionNotFound
from discord.ext.commands.errors import NoEntryPointError
from context import CustomContext
from tunebot.plugins import FileSystemPluginLoader
from tunebot.plugins import SimplePluginManager
from tunebot.redis import GlobalRedisAutoJoin
from tunebot.redis import GlobalRedisPlaylist
from tunebot.redis import GlobalRedisPlaylistSource
from tunebot.redis import GlobalRedisUtils
if TYPE_CHECKING:
from tunebot import PluginManagerBase
from tunebot import GlobalPlaylist
from tunebot import GlobalPlaylistSource
from tunebot import GlobalAutoJoin
from tunebot import PluginLoaderBase
from tunebot import PluginManagerBase
from tunebot import GlobalUtils
ColorDict = dict[str, "Color"]
class ChristmasBot(commands.Bot):
class TuneBot(commands.Bot):
lavalink: lavalink.Client
invite_link: str
invite_link: str = ""
initial_cog_names: list[str]
colors: ColorDict
global_autojoin: "GlobalAutoJoin"
global_playlist: "GlobalPlaylist"
global_playlist_source: "GlobalPlaylistSource"
global_utils: "GlobalUtils"
plugin_loader: "PluginLoaderBase"
plugin_manager: "PluginManagerBase"
def __init__(self, config: Dict[Any, Any]):
intents = discord.Intents(
voice_states=True, guild_messages=True, guilds=True, messages=True
voice_states=True,
guild_messages=True,
guilds=True,
messages=True,
members=True,
)
self.rpc_is_help_message = True
self.update_status.start()
self.config = config
self.initial_cog_names: List[str] = self.config.get("cogs", [])
self.colors: Dict[str, Color] = self.process_colours(config.get("colors", []))
self.initial_cog_names = self.config.get("cogs", [])
self.colors = self.process_colours(config.get("colors", []))
self.redis_prefix = self.config["redis_prefix"]
self._redis_client: Redis = aioredis.from_url(
self.config["redis_url"], encoding="utf-8", decode_responses=True
)
self.invite_link: str = ""
self.global_autojoin = GlobalRedisAutoJoin(
self._redis_client, self.redis_prefix
)
self.global_playlist = GlobalRedisPlaylist(
self._redis_client, self.redis_prefix
)
self.global_playlist_source = GlobalRedisPlaylistSource(
self._redis_client, self.redis_prefix
)
self.global_utils = GlobalRedisUtils(self._redis_client, self.redis_prefix)
self.plugin_loader = FileSystemPluginLoader(self)
self.plugin_manager = SimplePluginManager(self)
slash_guilds = None
if len(self.config["slash_command_guilds"]) > 0:
@@ -50,6 +97,7 @@ class ChristmasBot(commands.Bot):
super().__init__(
command_prefix=self.prefix_callable,
owner_ids=self.config["owner_ids"],
description=self.config["info"]["description"],
case_insensitive=False,
fetch_offline_members=False,
@@ -61,23 +109,24 @@ class ChristmasBot(commands.Bot):
self.loop.create_task(self.async_init())
async def async_init(self):
await self.load_cogs(self.initial_cog_names)
self.init_plugins()
self.load_cogs(self.initial_cog_names)
async def prefix_callable(self, _, msg: Message) -> List[str]:
return commands.when_mentioned_or(*self.config["prefixes"])(self, msg)
async def load_cogs(self, cog_names: Sequence[str]):
def load_cogs(self, cog_names: Sequence[str]):
for cog in cog_names:
try:
self.load_extension(cog)
print(f"Succesfully loaded extension {cog}.")
print(f"[✓] loaded extension: {cog}.")
except (
ExtensionNotFound,
ExtensionAlreadyLoaded,
NoEntryPointError,
ExtensionFailed,
) as e:
print(f"Failed to load extension {cog}.\n\t{e}", file=sys.stderr)
print(f"[x] failed loading extension: {cog}.\n\t{e}", file=sys.stderr)
async def on_ready(self):
self.invite_link = f"https://discord.com/oauth2/authorize?client_id={self.user.id}&permissions=3230720&scope=bot%20applications.commands"
@@ -86,7 +135,13 @@ class ChristmasBot(commands.Bot):
print(f"Version: {discord.__version__}")
print(f"Invite: {self.invite_link}")
def process_colours(self, colors: Dict[str, str]) -> Dict[str, Color]:
ll = self.config["lavalink"]
self.lavalink = lavalink.Client(self.user.id)
self.lavalink.add_node(
ll["host"], ll["port"], ll["password"], ll["region"], ll["name"]
)
def process_colours(self, colors: Dict[str, str]) -> ColorDict:
colour_dict: Dict[str, Color] = {}
for name, color in colors.items():
colour_dict[name] = Color(int(color, 16))
@@ -95,6 +150,19 @@ class ChristmasBot(commands.Bot):
async def get_context(self, message: Message, *, cls=CustomContext):
return await super().get_context(message, cls=cls)
def init_plugins(self):
for plug_id, plug_conf in self.config["plugins"].items():
if not plug_conf.get("enabled"):
continue
try:
plugin = self.plugin_loader.load_plugin(plug_conf)
self.plugin_manager.enable_plugin(plug_id, plugin)
print(f"[✓] loaded plugin: {plug_id}")
except Exception as e:
self.plugin_manager.remove_plugin(plug_id)
print(f"[x] failed loading plugin: {plug_id}\n{e}")
@tasks.loop(seconds=30)
async def update_status(self):
await self.wait_until_ready()
@@ -110,9 +178,6 @@ class ChristmasBot(commands.Bot):
await self.change_presence(activity=activity)
config = json.load(open("config.json", "r", encoding="utf-8"))
redis_prefix = config["redis_prefix"]
if __name__ == "__main__":
try:
import uvloop
@@ -122,5 +187,10 @@ if __name__ == "__main__":
except ModuleNotFoundError:
pass
config_path = "config.json"
if len(sys.argv) > 1:
config_path = sys.argv[1]
config = json.load(open(config_path, "r", encoding="utf-8"))
token = config.pop("token")
ChristmasBot(config).run(token, reconnect=True)
TuneBot(config).run(token, reconnect=True)
+15 -18
View File
@@ -6,14 +6,12 @@ import discord
import humanize
import lavalink
from discord.ext import commands
from discord.ext import tasks
from discord.ext.commands import Context
from bot import ChristmasBot
from bot import TuneBot
from context import CustomContext
from utils.database import AutoJoin
from utils.EmbedGenerator import EmbedGenerator
from utils.classes import BaseCog
from utils.EmbedGenerator import EmbedGenerator
from utils.paginator import HelpPaginator
@@ -53,26 +51,25 @@ class InformationCog(BaseCog, name="Information"):
async def wlinfo(self, ctx: CustomContext):
"""Retrieve various node/server/player information"""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
node = player.node
nodes = self.bot.lavalink.node_manager.available_nodes
used = humanize.naturalsize(node.stats.memory_used)
total = humanize.naturalsize(node.stats.memory_allocated)
free = humanize.naturalsize(node.stats.memory_free)
cpu = node.stats.cpu_cores
used = humanize.naturalsize(sum([n.stats.memory_used for n in nodes]))
total = humanize.naturalsize(sum([n.stats.memory_allocated for n in nodes]))
free = humanize.naturalsize(sum([n.stats.memory_free for n in nodes]))
cpu = sum([n.stats.cpu_cores for n in nodes])
fmt = (
f"**WaveLink:** `{lavalink.__version__}`\n\n"
f"Connected to `{len(self.bot.lavalink.nodes)}` nodes.\n"
f"Best available Node `{self.bot.lavalink.get_best_node().__repr__()}`\n"
f"`{len(self.bot.lavalink.players)}` players are distributed on nodes.\n"
f"`{node.stats.players}` players are distributed on server.\n"
f"`{node.stats.playing_players}` players are playing on server.\n\n"
f"**Lavalink:** `{lavalink.__version__}`\n\n"
f"Connected to `{len(self.bot.lavalink.node_manager.available_nodes)}` nodes.\n"
# f"Best available Node `{self.bot.lavalink.node_manager.find_ideal_node().name.__repr__()}`\n"
f"`{len(self.bot.lavalink.player_manager.players)}` players are distributed on nodes.\n"
f"`{sum([n.stats.players for n in nodes])}` players are distributed on server.\n"
f"`{sum([n.stats.playing_players for n in nodes])}` players are playing on server.\n\n"
f"Server Memory: `{used}/{total}` | `({free} free)`\n"
f"Server CPU: `{cpu}`\n\n"
f"Server Uptime: `{datetime.timedelta(milliseconds=node.stats.uptime)}`"
# f"Server Uptime: `{datetime.timedelta(milliseconds=node.stats.uptime)}`"
)
await ctx.send(fmt)
AutoJoin.get_channels()
@commands.command(name="help", aliases=["about", "info"])
@commands.cooldown(1, 1, commands.BucketType.user)
@@ -123,6 +120,6 @@ class InformationCog(BaseCog, name="Information"):
await ctx.send(embed=embed)
def setup(bot: ChristmasBot):
def setup(bot: TuneBot):
bot.remove_command("help")
bot.add_cog(InformationCog(bot))
+65 -54
View File
@@ -1,27 +1,28 @@
import asyncio
import datetime
import re
from typing import Any
from typing import Optional
from typing import TYPE_CHECKING
import discord
from discord.channel import TextChannel
from discord.ext.commands.context import Context
from discord.ext.commands.errors import CommandError
import lavalink
from discord import Embed
from discord.channel import TextChannel
from discord.ext import commands
from discord.ext.commands.context import Context
from discord.ext.commands.errors import CommandError
from lavalink.models import AudioTrack
from lavalink.models import DefaultPlayer
from bot import ChristmasBot
from utils.classes import BaseCog
from bot import TuneBot
from context import CustomContext
from utils.database import AutoJoin
from utils.database import Playlist
from utils.exceptions import EmbeddedCommandException
from tunebot.plugins import ServiceEvent
from utils.classes import BaseCog
from utils.EmbedGenerator import EmbedGenerator
from utils.exceptions import EmbeddedCommandException
if TYPE_CHECKING:
from discord import VoiceChannel
url_rx = re.compile(r"https?://(?:www\.)?.+")
@@ -78,25 +79,11 @@ class LavalinkVoiceClient(discord.VoiceClient):
class Music(BaseCog):
@commands.Cog.listener()
async def on_ready(self):
if not hasattr(
self.bot, "lavalink"
): # This ensures the client isn't overwritten during cog reloads.
self.bot.lavalink = lavalink.Client(self.bot.user.id)
ll = self.bot.config["lavalink"]
self.bot.lavalink.add_node(
ll["host"], ll["port"], ll["password"], ll["region"], ll["name"]
)
self.bot.lavalink.add_event_hook(self.track_hook)
await self.async_init()
async def async_init(self):
redis_result = await AutoJoin.get_channels(self.bot._redis_client)
while len(self.bot.lavalink.node_manager.available_nodes) == 0:
while not self.is_lavalink_ready():
await asyncio.sleep(1)
self.bot.lavalink.add_event_hook(self.track_hook)
redis_result = await self.bot.global_autojoin.fetch_channels()
for guild_id, (voicechannel_id, textchannel_id) in redis_result.items():
player = self.bot.lavalink.player_manager.create(guild_id)
player.store("channel", textchannel_id)
@@ -112,11 +99,12 @@ class Music(BaseCog):
await textchannel.send("Automatically joined the voice channel")
async def fill_player_queue(self, player: DefaultPlayer, buffer: Optional[int] = 1):
queries = await Playlist.random(self.bot._redis_client, buffer)
# Get the results for the query from Lavalink.
queries = await self.bot.global_playlist.pick_random(buffer)
failed_queries: list[str] = []
for query in queries:
result = await player.node.get_tracks(query)
if not result or not result["tracks"]:
failed_queries.append(query)
continue
track = lavalink.models.AudioTrack(
@@ -124,6 +112,10 @@ class Music(BaseCog):
)
player.add(requester=self.bot.user.id, track=track)
if len(failed_queries) > 0:
await self.bot.global_playlist.remove_tracks(failed_queries)
await self.fill_player_queue(player, len(failed_queries))
async def create_track_embed(self, track: AudioTrack) -> Embed:
embed_color = self.bot.colors["embed"]
embed = discord.Embed(
@@ -155,7 +147,7 @@ class Music(BaseCog):
# This is essentially the same as `@commands.guild_only()`
# except it saves us repeating ourselves (and also a few lines).
if not hasattr(self.bot, "lavalink"):
if not self.is_lavalink_ready():
await ctx.send("Still starting please wait a moment.")
if guild_check:
@@ -230,26 +222,50 @@ class Music(BaseCog):
guild = self.bot.get_guild(guild_id)
await guild.voice_client.disconnect(force=True)
elif isinstance(event, lavalink.events.TrackStartEvent):
channel_id = int(event.player.fetch("channel"))
channel: TextChannel = self.bot.get_channel(channel_id)
embed = await self.create_track_embed(event.player.current)
await channel.send(embed=embed)
if channel := event.player.fetch("channel"):
channel_id = int(channel)
channel: TextChannel = self.bot.get_channel(channel_id)
embed = await self.create_track_embed(event.player.current)
await channel.send(embed=embed)
elif isinstance(event, lavalink.events.TrackEndEvent):
await self.fill_player_queue(event.player, 1)
if event.reason == "FINISHED":
if event.player.channel_id:
channel_id = int(event.player.channel_id)
voice_channel: "VoiceChannel" = self.bot.get_channel(channel_id)
in_voice: list[int] = []
for member in voice_channel.members:
if member.bot:
continue
in_voice.append(member.id)
payload: dict[str, Any] = {
"last_track": event.track,
"in_voice": in_voice,
}
s = ServiceEvent.TRACK_ENDED
await self.bot.plugin_manager.dispatch(s, payload)
else:
fmt = f"Failed dispatching for TrackEnd event, missing channel_id on player"
print(fmt)
@commands.command(name="connect", aliases=["p", "play", "join"])
async def play(self, ctx: CustomContext):
"""Start the radio"""
# Get the player for this guild from cache.
player = ctx.get_player()
if player.is_connected:
if ctx.player.is_connected:
await ctx.send("Already connected")
return
await self.fill_player_queue(player, self.bot.config["queue_buffer_size"]+1)
if not player.is_playing:
await player.play()
await self.fill_player_queue(
ctx.player, self.bot.config["queue_buffer_size"] + 1
)
if not ctx.player.is_playing:
await ctx.player.play()
await EmbedGenerator.Title(ctx, "*⃣ | Connected.")
return
@@ -257,21 +273,19 @@ class Music(BaseCog):
@commands.command(name="skip", aliases=["next"])
async def skip(self, ctx: CustomContext):
"""Skip the current song"""
player = ctx.get_player()
await player.skip()
await ctx.player.skip()
await ctx.send("Skipped current song")
@commands.command(name="queue")
async def queue(self, ctx: CustomContext):
"""Display the current radio queue"""
player = ctx.get_player()
embed_color = self.bot.colors["embed"]
embed = Embed(title="Coming Up...", colour=embed_color)
if len(player.queue) > 0:
if len(ctx.player.queue) > 0:
embed.description = "\n".join(
f"{index}. [{track.title}]({track.uri})"
for index, track in enumerate(player.queue, 1)
for index, track in enumerate(ctx.player.queue, 1)
)
else:
embed.description = "We are still determining a playlist"
@@ -281,11 +295,9 @@ class Music(BaseCog):
@commands.command(name="disconnect", aliases=["dc", "stop"])
async def disconnect(self, ctx: CustomContext):
"""Disconnects the radio from the channel"""
player = ctx.get_player()
if not ctx.author.voice or (
player.is_connected
and ctx.author.voice.channel.id != int(player.channel_id)
ctx.player.is_connected
and ctx.author.voice.channel.id != int(ctx.player.channel_id)
):
# Abuse prevention. Users not in voice channels, or not in the same voice channel as the bot
# may not disconnect the bot.
@@ -294,20 +306,19 @@ class Music(BaseCog):
# Clear the queue to ensure old tracks don't start playing
# when someone else queues something.
player.queue.clear()
ctx.player.queue.clear()
# Stop the current track so Lavalink consumes less resources.
await player.stop()
await ctx.player.stop()
# Disconnect from the voice channel.
await ctx.voice_client.disconnect(force=True)
await EmbedGenerator.Title(ctx, "*⃣ | Disconnected.")
@commands.command(name="now")
async def now_playing(self, ctx: CustomContext):
player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
track = player.current
embed = await self.create_track_embed(track)
"""Displays information about the currently played track"""
embed = await self.create_track_embed(ctx.player.current)
await ctx.send(embed=embed)
def setup(bot: ChristmasBot):
def setup(bot: TuneBot):
bot.add_cog(Music(bot))
+3 -3
View File
@@ -12,12 +12,12 @@ import discord
from discord.ext import commands
from discord.ext.commands.context import Context
from bot import ChristmasBot
from bot import TuneBot
from utils.classes import BaseCog
class OwnerCog(BaseCog):
def __init__(self, bot: ChristmasBot):
def __init__(self, bot: TuneBot):
super().__init__(bot)
self._last_result = None
@@ -209,5 +209,5 @@ class OwnerCog(BaseCog):
await message.edit(content=f"```fix\n{content}\n```")
def setup(bot: ChristmasBot):
def setup(bot: TuneBot):
bot.add_cog(OwnerCog(bot))
+137 -20
View File
@@ -1,13 +1,12 @@
from discord.ext import commands
from utils.EmbedGenerator import EmbedGenerator
from utils.classes import BaseCog
from utils.database import AutoJoin
from bot import ChristmasBot
from discord.ext.commands import Context
from typing import Any
from bot import ChristmasBot
from discord.ext import commands
from discord.message import Message
from bot import TuneBot
from context import CustomContext
from utils.database import AutoJoin
from utils.classes import BaseCog
from utils.decorators import source_manager_only
from utils.EmbedGenerator import EmbedGenerator
@@ -19,29 +18,147 @@ class SettingsCog(BaseCog, name="Settings"):
await EmbedGenerator.Message(
ctx,
"Autojoin",
f"Usage:\n\n`{ctx.prefix}autojoin set`\n`{ctx.prefix}autojoin unset`",
f"Usage:\n\n`{ctx.prefix}autojoin enable`\n`{ctx.prefix}autojoin disable`",
)
@autojoin.command(name="enable")
@autojoin.command(name="enable", aliases=["set"])
@commands.has_permissions(manage_channels=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def autojoin_set(self, ctx: CustomContext):
"""Enable the bot automatically joining"""
voicechannel_id = ctx.author.voice.channel.id
textchannel_id = ctx.message.channel.id
await AutoJoin.update_channel(
ctx.get_redis(), ctx.guild.id, voicechannel_id, textchannel_id
)
await EmbedGenerator.Message(ctx, "Autojoin", "`enabled`")
voice_state = ctx.author.voice
if not voice_state:
embed = ctx.create_embed()
embed.title = "Please join a voice channel before running this command."
await ctx.send(embed=embed)
return
@autojoin.command(name="disable")
await ctx.autojoin.update(voice_state.channel.id, ctx.message.channel.id)
embed = ctx.create_embed()
embed.title = f"AutoJoin enabled for #{voice_state.channel.name}"
await ctx.send(embed=embed)
@autojoin.command(name="disable", aliases=["unset"])
@commands.has_permissions(manage_channels=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def autojoin_del(self, ctx: CustomContext):
"""Disable the bot automatically joining"""
await AutoJoin.del_channel(ctx.get_redis(), ctx.guild.id)
await EmbedGenerator.Message(ctx, "Autojoin", "`disabled`")
await ctx.autojoin.disable()
embed = ctx.create_embed()
embed.title = f"AutoJoin disabled"
await ctx.send(embed=embed)
@source_manager_only()
@commands.group(
name="source",
aliases=["src"],
invoke_without_command=True,
slash_command=False,
hidden=True,
)
async def source(self, ctx: CustomContext):
"""Displays all possible options for the `source` command"""
prefix = self.bot.config["prefixes"][0]
embed = ctx.create_embed()
embed.title = "All options:"
embed.description = f"```{prefix}source list\n{prefix}source add <url>\n{prefix}source remove <url>\n{prefix}source sync```"
await ctx.send(embed=embed)
@source_manager_only()
@source.command(name="remove")
async def source_remove(self, ctx: CustomContext, source_url: str):
"""Removes a source from the bot"""
if await ctx.playlist_source.remove(source_url):
prefix = self.bot.config["prefixes"][0]
embed = ctx.create_embed()
embed.title = "Removed source succesfully"
embed.description = (
f"Please use `{prefix}source sync` to persist these changes."
)
await ctx.send(embed=embed)
return
embed = ctx.create_embed()
embed.title = "Could not remove source, the specified source might not exist"
await ctx.send(embed=embed)
@source_manager_only()
@source.command(name="add")
async def source_add(self, ctx: CustomContext, source_url: str):
"""
Add's a source to the bot
Supported sources: YouTube, SoundCloud, Bandcamp, Vimeo, Twitch and HTTP(S) URL's
"""
embed = ctx.create_embed()
embed.title = "Started processing source"
message = await ctx.send(embed=embed)
query_result: Any = await self.bot.lavalink.get_tracks(source_url)
if query_result["loadType"] == "LOAD_FAILED":
embed = ctx.create_embed()
embed.title = "The specified URL is not a valid source"
embed.description = f"Supported sources: YouTube, SoundCloud, Bandcamp, Vimeo, Twitch and HTTP(S) URL's"
if isinstance(message, Message):
await message.edit(embed=embed)
else:
await ctx.send(embed=embed)
return
await ctx.playlist_source.add(source_url)
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
await self.bot.global_playlist.add_tracks(track_urls)
embed = ctx.create_embed()
embed.title = "Finished processing source"
embed.description = f"Added {len(track_urls)} tracks"
if isinstance(message, Message):
await message.edit(embed=embed)
else:
await ctx.send(embed=embed)
@source_manager_only()
@source.command(name="list", aliases=["ls"])
async def source_list(self, ctx: CustomContext):
"""Display a list of sources"""
# TODO: Implement pagination for sources
sources = await self.bot.global_playlist_source.fetch_sources()
if len(sources) > 0:
description = "\n".join([f"[{source}]({source})" for source in sources])
else:
description = "This bot has no sources yet"
embed = ctx.create_embed()
embed.title = "All sources:"
embed.description = description
await ctx.send(embed=embed)
@source_manager_only()
@source.command(name="sync")
async def source_sync(self, ctx: CustomContext):
"""Forcefully resyncs all sources"""
failed_sources: list[str] = []
await self.bot.global_playlist.clear()
sources = await self.bot.global_playlist_source.fetch_sources()
for source_url in sources:
query_result: Any = await self.bot.lavalink.get_tracks(source_url)
if query_result["loadType"] == "LOAD_FAILED":
failed_sources.append(source_url)
continue
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
await self.bot.global_playlist.add_tracks(track_urls)
embed = ctx.create_embed()
embed.title = f"Finished sync ({len(failed_sources)} issues)"
if len(failed_sources) > 0:
embed.description = "\n".join(
[f"[{source_url}]({source_url})" for source_url in failed_sources]
)
await ctx.send(embed=embed)
def setup(bot: ChristmasBot):
def setup(bot: TuneBot):
bot.add_cog(SettingsCog(bot))
+15 -1
View File
@@ -1,6 +1,7 @@
{
"token": "",
"owner_ids": [194545408960102400, 190875175460405249],
"manager_ids": [],
"prefixes": ["ck!"],
"redis_url": "",
"redis_prefix": "",
@@ -21,5 +22,18 @@
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"],
"slash_command_guilds": [],
"queue_buffer_size": 5,
"slash_descriptions": {}
"slash_descriptions": {},
"plugins": {
"lastfm-scrobbler": {
"services": ["plugins.lastfm_scrobbler.service"],
"cogs": ["plugins.lastfm_scrobbler.cog"],
"config": {
"lastfm_api_key": "",
"lastfm_api_secret": "",
"session_key_table": "",
"auth_url": ""
},
"enabled": false
}
}
}
+51 -3
View File
@@ -1,15 +1,63 @@
from typing import Any
from typing import TYPE_CHECKING
from aioredis.client import Redis
from discord import Embed
from discord.ext import commands
from discord.ext.commands.errors import CommandInvokeError
from lavalink.models import DefaultPlayer
from tunebot.redis import RedisAutoJoin
from tunebot.redis import RedisPlaylistSource
if TYPE_CHECKING:
from bot import TuneBot
from tunebot import PlaylistSource
from tunebot import AutoJoin
from utils.classes import BaseCog
from tunebot.plugins import ServiceEvent
AnyDict = dict[Any, Any]
class CustomContext(commands.Context):
def get_redis(self) -> Redis:
bot: "TuneBot"
cog: "BaseCog"
async def dispatch(self, event: "ServiceEvent", payload: AnyDict):
await self.bot.plugin_manager.dispatch(event, payload)
@property
def redis(self) -> Redis:
return self.bot._redis_client
def get_player(self) -> DefaultPlayer:
if hasattr(self.bot, "lavalink"):
@property
def player(self) -> DefaultPlayer:
if self.cog.is_lavalink_ready():
return self.bot.lavalink.player_manager.get(self.guild.id)
raise CommandInvokeError("Lavalink is still starting up.")
def create_embed(self) -> Embed:
bot: TuneBot = self.bot
color = bot.colors["embed"]
avatar = None
if avatar_asset := self.author.avatar:
avatar = avatar_asset.with_static_format("jpeg")
embed = Embed(color=color)
embed.set_footer(text=f"Requested by: {self.author}", icon_url=avatar)
return embed
@property
def playlist_source(self) -> "PlaylistSource":
if not hasattr(self, "_playlist_source"):
self._playlist_source = RedisPlaylistSource(self)
return self._playlist_source
@property
def autojoin(self) -> "AutoJoin":
if not hasattr(self, "_autojoin"):
self._autojoin = RedisAutoJoin(self)
return self._autojoin
+46
View File
@@ -0,0 +1,46 @@
from typing import TYPE_CHECKING
from discord.ext import commands
from context import CustomContext
from utils.classes import PluginCog
if TYPE_CHECKING:
from bot import TuneBot
class LastFMScrobblerCog(PluginCog, name="LastFMScrobbler"):
def __init__(self, bot: "TuneBot") -> None:
super().__init__(bot)
self.plugin = self.get_plugin_instance("lastfm-scrobbler")
@commands.group(name="lfm", invoke_without_command=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def lfm(self, ctx: CustomContext):
"""Enable/Disable LastFM Scrobbling"""
embed = ctx.create_embed()
embed.title = f"LastFM scrobbler usage:"
embed.description = f"```{ctx.prefix}lfm enable\n{ctx.prefix}lfm del```"
await ctx.send(embed=embed)
@lfm.command(name="enable", aliases=["set"])
async def lfm_aut(self, ctx: CustomContext):
"""Enable LastFM Scrobbling"""
embed = ctx.create_embed()
embed.title = f"Start scrobbling"
auth_url = self.plugin.config["auth_url"] + "?user_id=" + str(ctx.author.id)
embed.description = f"[Login]({auth_url})"
await ctx.send(embed=embed)
@lfm.command(name="delete", aliases=["del"])
async def lfm_del(self, ctx: CustomContext):
"""Disable LastFM Scrobbling"""
db_key = self.plugin.config["session_key_table"]
await self.bot.global_utils.raw_table_del_entry(db_key, [ctx.author.id])
embed = ctx.create_embed()
embed.title = f"Stopped scrobbling"
await ctx.send(embed=embed)
def setup(bot: "TuneBot"):
bot.add_cog(LastFMScrobblerCog(bot))
+84
View File
@@ -0,0 +1,84 @@
import hashlib
import time
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING
import aiohttp
from lavalink.models import AudioTrack
from tunebot.plugins import ServiceEvent
if TYPE_CHECKING:
from bot import TuneBot
AnyDict = dict[Any, Any]
@dataclass
class Track:
name: str
artist: str
class LastFMScrobbler:
api_url = "http://ws.audioscrobbler.com/2.0/"
def __init__(self, bot: "TuneBot", config: AnyDict) -> None:
self.bot = bot
self.plug_conf = config
async def on_dispatch(self, event: "ServiceEvent", payload: AnyDict):
if event == ServiceEvent.TRACK_ENDED:
_track: AudioTrack = payload.get("last_track", None)
if not _track:
print("LastFMScrobbler: Could not find required field `last_track`")
return
# TODO: expand parsing options
artist, track_name = _track.title.split(" - ", 1)
track = Track(track_name, artist)
in_voice = payload.get("in_voice", [])
for session_key in await self.fetch_lastfm_sessions(in_voice):
if not session_key:
continue
await self.scrobble(session_key, track)
async def fetch_lastfm_sessions(self, user_ids: list[int]) -> list[str]:
table_name = self.plug_conf["config"]["session_key_table"]
result: list[str] = await self.bot.global_utils.raw_table_lookup(
table_name, user_ids
)
return result
async def scrobble(self, session_key: str, track: Track):
params: AnyDict = {
"method": "track.scrobble",
"timestamp": str(int(time.time() - 30)),
"track": track.name,
"artist": track.artist,
"sk": session_key,
}
resp = await self.lastfm_request(params)
if resp.status != 200:
fmt = f"Failed to scrobble for user {session_key} on track: {track.artist} - {track.name}"
print(fmt)
async def lastfm_request(self, params: AnyDict) -> aiohttp.ClientResponse:
params["api_key"] = self.plug_conf["config"]["lastfm_api_key"]
params = {key: params[key] for key in sorted(params)}
secret = self.plug_conf["config"]["lastfm_api_secret"]
sig_str = "".join(key + params[key] for key in params.keys()) + secret
params["api_sig"] = hashlib.md5(sig_str.encode("utf8")).hexdigest()
params["format"] = "json"
async with aiohttp.ClientSession() as sess:
async with sess.post(self.api_url, params=params) as resp:
return resp
def setup(bot: "TuneBot", config: AnyDict):
return LastFMScrobbler(bot, config)
Generated
+272 -46
View File
@@ -31,6 +31,18 @@ typing-extensions = "*"
[package.extras]
hiredis = ["hiredis (>=1.0)"]
[[package]]
name = "aiotube"
version = "1.3.5"
description = "Get YouTube Public Data without YouTubeAPI"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytube = "*"
urllib3 = "*"
[[package]]
name = "async-timeout"
version = "3.0.1"
@@ -67,7 +79,7 @@ testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3
[[package]]
name = "black"
version = "21.9b0"
version = "21.10b0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
@@ -87,11 +99,19 @@ typing-extensions = [
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.2)"]
python2 = ["typed-ast (>=1.4.3)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "cffi"
version = "1.15.0"
@@ -119,6 +139,17 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.0.3"
@@ -236,6 +267,14 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "mutagen"
version = "1.45.1"
description = "read and write audio tags for many formats"
category = "dev"
optional = false
python-versions = ">=3.5, <4"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
@@ -298,12 +337,20 @@ virtualenv = ">=20.0.8"
[[package]]
name = "pycparser"
version = "2.20"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycryptodomex"
version = "3.11.0"
description = "Cryptographic library for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pynacl"
version = "1.4.0"
@@ -320,6 +367,14 @@ six = "*"
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"]
[[package]]
name = "pytube"
version = "11.0.1"
description = "Python 3 library for downloading YouTube Videos."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pyyaml"
version = "6.0"
@@ -328,14 +383,43 @@ category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "redis"
version = "3.5.3"
description = "Python client for Redis key-value store"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
hiredis = ["hiredis (>=0.1.3)"]
[[package]]
name = "regex"
version = "2021.10.23"
version = "2021.11.2"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.26.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "six"
version = "1.16.0"
@@ -368,6 +452,19 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "uvloop"
version = "0.16.0"
@@ -381,6 +478,14 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
[[package]]
name = "websockets"
version = "10.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "virtualenv"
version = "20.10.0"
@@ -412,10 +517,23 @@ python-versions = ">=3.5"
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
name = "yt-dlp"
version = "2021.10.22"
description = "Command-line program to download videos from YouTube.com and many other other video platforms."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mutagen = "*"
pycryptodomex = "*"
websockets = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "7428479732f5183e24be61ddeee7230bafdd6b76920e2d6d548c6a091e570111"
content-hash = "87b61ad577bb9413177b2749b0c4b30026aa512bf3a6eb62e74c0ef7e2470ed4"
[metadata.files]
aiohttp = [
@@ -437,6 +555,10 @@ aioredis = [
{file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"},
{file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"},
]
aiotube = [
{file = "aiotube-1.3.5-py3-none-any.whl", hash = "sha256:4dcef22dd27e6229b82186555c7f4779b62f751d669b9517b88d8d7af977338f"},
{file = "aiotube-1.3.5.tar.gz", hash = "sha256:3b015060480136812249433b02b5d869ad62a82916a92fe741bf4f907b7a948b"},
]
async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
@@ -450,8 +572,12 @@ attrs = [
{file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
]
black = [
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
{file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"},
{file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"},
]
certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
@@ -513,6 +639,10 @@ chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
{file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
@@ -564,6 +694,10 @@ multidict = [
{file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"},
{file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"},
]
mutagen = [
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
@@ -610,8 +744,40 @@ pre-commit = [
{file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pycryptodomex = [
{file = "pycryptodomex-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7abfd84a362e4411f7c5f5758c18cbf377a2a2be64b9232e78544d75640c677e"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6a76d7821ae43df8a0e814cca32114875916b9fc2158603b364853de37eb9002"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:1580db5878b1d16a233550829f7c189c43005f7aa818f2f95c7dddbd6a7163cc"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c825611a951baad63faeb9ef1517ef96a20202d6029ae2485b729152cc703fab"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7cc5ee80b2d5ee8f59a761741cfb916a068c97cac5e700c8ce01e1927616aa2f"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:fbe09e3ae95f47c7551a24781d2e348974cde4a0b33bc3b1566f6216479db2b1"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:9eace1e5420abc4f9e76de01e49caca349b7c80bda9c1643193e23a06c2a332c"},
{file = "pycryptodomex-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:adc25aa8cfc537373dd46ae97863f16fd955edee14bf54d3eb52bde4e4ac8c7b"},
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cf30b5e03d974874185b989839c396d799f6e2d4b4d5b2d8bd3ba464eb3cc33f"},
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c91772cf6808cc2d80279e80b491c48cb688797b6d914ff624ca95d855c24ee5"},
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c391ec5c423a374a36b90f7c8805fdf51a0410a2b5be9cebd8990e0021cb6da4"},
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:64a83ab6f54496ab968a6f21a41a620afe0a742573d609fd03dcab7210645153"},
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:252ac9c1e1ae1c256a75539e234be3096f2d100b9f4bae42ef88067787b9b249"},
{file = "pycryptodomex-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bf2ea67eaa1fff0aecef6da881144f0f91e314b4123491f9a4fa8df0598e48fe"},
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:fe2b8c464ba335e71aed74f830bf2b2881913f8905d166f9c0fe06ca44a1cb5e"},
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:ff0826f3886e85708a0e8ef7ec47020723b998cfed6ae47962d915fcb89ec780"},
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:1d4d13c59d2cfbc0863c725f5812d66ff0d6836ba738ef26a52e1291056a1c7c"},
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:2b586d13ef07fa6197b6348a48dbbe9525f4f496205de14edfa4e91d99e69672"},
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:f35ccfa44a1dd267e392cd76d8525cfcfabee61dd070e15ad2119c54c0c31ddf"},
{file = "pycryptodomex-3.11.0-cp35-abi3-win32.whl", hash = "sha256:5baf690d27f39f2ba22f06e8e32c5f1972573ca65db6bdbb8b2c7177a0112dab"},
{file = "pycryptodomex-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:919cadcedad552e78349d1626115cfd246fc03ad469a4a62c91a12204f0f0d85"},
{file = "pycryptodomex-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:c10b2f6bcbaa9aa51fe08207654100074786d423b03482c0cbe44406ca92d146"},
{file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:91662b27f5aa8a6d2ad63be9a7d1a403e07bf3c2c5b265a7cc5cbadf6f988e06"},
{file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:207e53bdbf3a26de6e9dcf3ebaf67ba70a61f733f84c464eca55d278211c1b71"},
{file = "pycryptodomex-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:1dd4271d8d022216533c3547f071662b44d703fd5dbb632c4b5e77b3ee47567f"},
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c43ddcff251e8b427b3e414b026636617276e008a9d78a44a9195d4bdfcaa0fe"},
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ef25d682d0d9ab25c5022a298b5cba9084c7b148a3e71846df2c67ea664eacc7"},
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:4c7c6418a3c08b2ebfc2cf50ce52de267618063b533083a2c73b40ec54a1b6f5"},
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:15d25c532de744648f0976c56bd10d07b2a44b7eb2a6261ffe2497980b1102d8"},
{file = "pycryptodomex-3.11.0.tar.gz", hash = "sha256:0398366656bb55ebdb1d1d493a7175fc48ade449283086db254ac44c7d318d6d"},
]
pynacl = [
{file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"},
@@ -633,6 +799,10 @@ pynacl = [
{file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"},
{file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"},
]
pytube = [
{file = "pytube-11.0.1-py3-none-any.whl", hash = "sha256:d4dfea7394d7662edac3831432b349b2afac984e0d0bc4bdb611ec1f3fc16318"},
{file = "pytube-11.0.1.tar.gz", hash = "sha256:47643a6ff553cbc4d6be748ff14c9c45f79984e15005adff25d62b18110abe43"},
]
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
@@ -668,43 +838,64 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
redis = [
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
]
regex = [
{file = "regex-2021.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45b65d6a275a478ac2cbd7fdbf7cc93c1982d613de4574b56fd6972ceadb8395"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74d071dbe4b53c602edd87a7476ab23015a991374ddb228d941929ad7c8c922e"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34d870f9f27f2161709054d73646fc9aca49480617a65533fc2b4611c518e455"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fb698037c35109d3c2e30f2beb499e5ebae6e4bb8ff2e60c50b9a805a716f79"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb46b542133999580ffb691baf67410306833ee1e4f58ed06b6a7aaf4e046952"},
{file = "regex-2021.10.23-cp310-cp310-win32.whl", hash = "sha256:5e9c9e0ce92f27cef79e28e877c6b6988c48b16942258f3bc55d39b5f911df4f"},
{file = "regex-2021.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ab7c5684ff3538b67df3f93d66bd3369b749087871ae3786e70ef39e601345b0"},
{file = "regex-2021.10.23-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de557502c3bec8e634246588a94e82f1ee1b9dfcfdc453267c4fb652ff531570"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee684f139c91e69fe09b8e83d18b4d63bf87d9440c1eb2eeb52ee851883b1b29"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5095a411c8479e715784a0c9236568ae72509450ee2226b649083730f3fadfc6"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b568809dca44cb75c8ebb260844ea98252c8c88396f9d203f5094e50a70355f"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eb672217f7bd640411cfc69756ce721d00ae600814708d35c930930f18e8029f"},
{file = "regex-2021.10.23-cp36-cp36m-win32.whl", hash = "sha256:a7a986c45d1099a5de766a15de7bee3840b1e0e1a344430926af08e5297cf666"},
{file = "regex-2021.10.23-cp36-cp36m-win_amd64.whl", hash = "sha256:6d7722136c6ed75caf84e1788df36397efdc5dbadab95e59c2bba82d4d808a4c"},
{file = "regex-2021.10.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f665677e46c5a4d288ece12fdedf4f4204a422bb28ff05f0e6b08b7447796d1"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:450dc27483548214314640c89a0f275dbc557968ed088da40bde7ef8fb52829e"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:129472cd06062fb13e7b4670a102951a3e655e9b91634432cfbdb7810af9d710"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a940ca7e7189d23da2bfbb38973832813eab6bd83f3bf89a977668c2f813deae"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530fc2bbb3dc1ebb17f70f7b234f90a1dd43b1b489ea38cea7be95fb21cdb5c7"},
{file = "regex-2021.10.23-cp37-cp37m-win32.whl", hash = "sha256:ded0c4a3eee56b57fcb2315e40812b173cafe79d2f992d50015f4387445737fa"},
{file = "regex-2021.10.23-cp37-cp37m-win_amd64.whl", hash = "sha256:391703a2abf8013d95bae39145d26b4e21531ab82e22f26cd3a181ee2644c234"},
{file = "regex-2021.10.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be04739a27be55631069b348dda0c81d8ea9822b5da10b8019b789e42d1fe452"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ec99df95003f56edcd307db44f06fbeb708c4ccdcf940478067dd62353181e"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d1cdcda6bd16268316d5db1038965acf948f2a6f43acc2e0b1641ceab443623"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c186691a7995ef1db61205e00545bf161fb7b59cdb8c1201c89b333141c438a"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b20f544cbbeffe171911f6ce90388ad36fe3fad26b7c7a35d4762817e9ea69c"},
{file = "regex-2021.10.23-cp38-cp38-win32.whl", hash = "sha256:c0938ddd60cc04e8f1faf7a14a166ac939aac703745bfcd8e8f20322a7373019"},
{file = "regex-2021.10.23-cp38-cp38-win_amd64.whl", hash = "sha256:56f0c81c44638dfd0e2367df1a331b4ddf2e771366c4b9c5d9a473de75e3e1c7"},
{file = "regex-2021.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80bb5d2e92b2258188e7dcae5b188c7bf868eafdf800ea6edd0fbfc029984a88"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1dae12321b31059a1a72aaa0e6ba30156fe7e633355e445451e4021b8e122b6"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f2b59c28afc53973d22e7bc18428721ee8ca6079becf1b36571c42627321c65"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d134757a37d8640f3c0abb41f5e68b7cf66c644f54ef1cb0573b7ea1c63e1509"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0dcc0e71118be8c69252c207630faf13ca5e1b8583d57012aae191e7d6d28b84"},
{file = "regex-2021.10.23-cp39-cp39-win32.whl", hash = "sha256:a30513828180264294953cecd942202dfda64e85195ae36c265daf4052af0464"},
{file = "regex-2021.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:0f7552429dd39f70057ac5d0e897e5bfe211629652399a21671e53f2a9693a4e"},
{file = "regex-2021.10.23.tar.gz", hash = "sha256:f3f9a91d3cc5e5b0ddf1043c0ae5fa4852f18a1c0050318baf5fc7930ecc1f9c"},
{file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"},
{file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"},
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"},
{file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"},
{file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"},
{file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"},
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"},
{file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"},
{file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"},
{file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"},
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"},
{file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"},
{file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"},
{file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"},
{file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"},
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"},
{file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"},
{file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"},
{file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"},
{file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"},
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"},
{file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"},
{file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"},
{file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"},
]
requests = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -723,6 +914,10 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
urllib3 = [
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
uvloop = [
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
@@ -741,6 +936,33 @@ uvloop = [
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
]
websockets = [
{file = "websockets-10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da"},
{file = "websockets-10.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939"},
{file = "websockets-10.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8"},
{file = "websockets-10.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379"},
{file = "websockets-10.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7"},
{file = "websockets-10.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c"},
{file = "websockets-10.0-cp37-cp37m-win32.whl", hash = "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76"},
{file = "websockets-10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80"},
{file = "websockets-10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b"},
{file = "websockets-10.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537"},
{file = "websockets-10.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456"},
{file = "websockets-10.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4"},
{file = "websockets-10.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238"},
{file = "websockets-10.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539"},
{file = "websockets-10.0-cp38-cp38-win32.whl", hash = "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465"},
{file = "websockets-10.0-cp38-cp38-win_amd64.whl", hash = "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a"},
{file = "websockets-10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805"},
{file = "websockets-10.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37"},
{file = "websockets-10.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a"},
{file = "websockets-10.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1"},
{file = "websockets-10.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2"},
{file = "websockets-10.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474"},
{file = "websockets-10.0-cp39-cp39-win32.whl", hash = "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368"},
{file = "websockets-10.0-cp39-cp39-win_amd64.whl", hash = "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567"},
{file = "websockets-10.0.tar.gz", hash = "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002"},
]
virtualenv = [
{file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"},
{file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"},
@@ -764,3 +986,7 @@ yarl = [
{file = "yarl-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692"},
{file = "yarl-1.5.1.tar.gz", hash = "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6"},
]
yt-dlp = [
{file = "yt-dlp-2021.10.22.tar.gz", hash = "sha256:a24b9666bd2234149e4da8c4f16bb8e5f746c29428d12ee04fc1c11b5247a307"},
{file = "yt_dlp-2021.10.22-py2.py3-none-any.whl", hash = "sha256:4900fdfffa3de0b09a74f3d7fe98e4d5e21f3ce74db0c106c942614f2ebc3368"},
]
+8 -4
View File
@@ -1,8 +1,8 @@
[tool.poetry]
name = "christmasbot"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
name = "tunebot"
version = "1.0.0"
description = "A very configurable Discord radio"
authors = ["StrNophix <curious@duck.com>", "Matthww <hello@exobot.site>"]
license = "GPL-v3.0"
[tool.poetry.dependencies]
@@ -15,6 +15,10 @@ aioredis = "^2.0.0"
[tool.poetry.dev-dependencies]
black = {version = "^21.9b0", allow-prereleases = true}
redis = "^3.5.3"
aiotube = "^1.3.5"
requests = "^2.26.0"
yt-dlp = "^2021.10.22"
pre-commit = "^2.15.0"
[build-system]
+28
View File
@@ -0,0 +1,28 @@
import json
import subprocess
import sys
import redis
from yt_dlp import YoutubeDL
if len(sys.argv) < 2:
raise Exception("Expected youtube playlist/channel/video")
config_path = sys.argv[1]
config = json.load(open("config.json", "r", encoding="utf-8"))
redis_prefix = config["redis_prefix"]
redis_client = redis.from_url(config["redis_url"])
file = open(config_path, "r")
youtube_dl_opts = {}
vid_urls = subprocess.run(["yt-dlp", "--print", "id", f"{sys.argv[1]}"], capture_output=True).stdout.decode("utf-8")
for vid_url in vid_urls.split("\n"):
if vid_url == "":
continue
vid_url = "https://www.youtube.com/watch?v=" + vid_url
redis_client.sadd(f"{redis_prefix}:playlist", vid_url)
print(vid_url)
+22
View File
@@ -0,0 +1,22 @@
import json
import sys
import redis
from aiotube import Playlist
if len(sys.argv) < 2:
raise Exception("Expected path to file as argument")
config_path = sys.argv[1]
config = json.load(open("config.json", "r", encoding="utf-8"))
redis_prefix = config["redis_prefix"]
redis_client = redis.from_url(config["redis_url"])
file = open(config_path, "r")
for line in file:
playlist = Playlist(line)
for vid in playlist.videos():
yt_url = vid.url
redis_client.sadd(f"{redis_prefix}:playlist", yt_url)
print(yt_url)
View File
+1
View File
@@ -0,0 +1 @@
from tunebot.abc import *
+115
View File
@@ -0,0 +1,115 @@
from abc import ABC
from abc import abstractmethod
from typing import Any
from typing import Optional
from typing import Protocol
from typing import TYPE_CHECKING
from typing import Union
if TYPE_CHECKING:
from tunebot.plugins import ServiceEvent
from discord.ext.commands import Cog
AnyDict = dict[Any, Any]
class GlobalPlaylistSource(ABC):
@abstractmethod
async def fetch_sources(self) -> set[str]:
pass
class PlaylistSource(ABC):
@abstractmethod
async def add(self, source_url: str):
pass
@abstractmethod
async def remove(self, source_url: str) -> bool:
pass
class GlobalPlaylist(ABC):
@abstractmethod
async def pick_random(self, amount: Optional[int] = 1) -> set[str]:
pass
@abstractmethod
async def add_tracks(self, urls: list[str]):
pass
@abstractmethod
async def clear(self):
pass
@abstractmethod
async def remove_tracks(self, track_urls: list[str]):
pass
class GlobalAutoJoin(ABC):
@abstractmethod
async def fetch_channels(self) -> dict[str, list[str]]:
pass
class AutoJoin(ABC):
@abstractmethod
async def update(self, voice_channel_id: int, text_channel_id: int):
pass
@abstractmethod
async def disable(self):
pass
class ServiceBase(Protocol):
async def on_dispatch(self, event: "ServiceEvent", payload: AnyDict):
...
class PluginManagerBase(Protocol):
def get_plugin(self, name: str) -> Union["BasePluginInstance", None]:
...
def remove_plugin(self, name: str):
...
def enable_plugin(self, name: str, plugin: "BasePluginInstance"):
...
async def dispatch(self, event: "ServiceEvent", payload: AnyDict = {}):
...
class PluginLoaderBase(Protocol):
def load_plugin(self, plug_conf: AnyDict) -> "BasePluginInstance":
...
class GlobalUtils(Protocol):
async def raw_table_lookup(self, table_name: str, keys: list[Any]) -> list[Any]:
...
async def raw_table_del_entry(self, table_name: str, keys: list[Any]):
...
class BasePluginInstance(Protocol):
config: AnyDict
services: list["ServiceBase"]
cogs: list[str]
__all__ = (
"GlobalPlaylistSource",
"PlaylistSource",
"GlobalPlaylist",
"GlobalAutoJoin",
"AutoJoin",
"ServiceBase",
"PluginManagerBase",
"PluginLoaderBase",
"GlobalUtils",
"BasePluginInstance",
)
+6
View File
@@ -0,0 +1,6 @@
# noreorder
from tunebot.plugins.exceptions import *
from tunebot.plugins.events import *
from tunebot.plugins.plugin import *
from tunebot.plugins.loader import *
from tunebot.plugins.manager import *
+16
View File
@@ -0,0 +1,16 @@
from enum import auto
from enum import Enum
class ServiceEvent(Enum):
"""
This Enum contains all possible events that can be dispatched to services
Args:
Enum ([type]): [description]
"""
TRACK_ENDED = auto()
__all__ = ("ServiceEvent",)
+5
View File
@@ -0,0 +1,5 @@
class PluginInitFailed(Exception):
pass
__all__ = ("PluginInitFailed",)
+42
View File
@@ -0,0 +1,42 @@
import importlib
from typing import Any
from typing import TYPE_CHECKING
from tunebot.plugins import PluginInstance
from tunebot.plugins.exceptions import PluginInitFailed
if TYPE_CHECKING:
from tunebot import BasePluginInstance
from tunebot import ServiceBase
from bot import TuneBot
AnyDict = dict[Any, Any]
class FileSystemPluginLoader:
def __init__(self, bot: "TuneBot") -> None:
self.bot = bot
def load_plugin(self, plug_conf: AnyDict) -> "BasePluginInstance":
"""
Loads a plugin from configuration and returns a sequence of Services
Raises:
PluginInitFailed: [description]
Returns:
tuple[list["ServiceBase"], list[str]]: [description]
"""
services: list["ServiceBase"] = []
for service_location in plug_conf["services"]:
module = importlib.import_module(service_location)
if not hasattr(module, "setup"):
raise PluginInitFailed('Failed to find setup() for "{location}"')
services.append(module.setup(self.bot, plug_conf))
return PluginInstance(plug_conf["config"], services, plug_conf["cogs"])
__all__ = ("FileSystemPluginLoader",)
+79
View File
@@ -0,0 +1,79 @@
from typing import Any
from typing import TYPE_CHECKING
from typing import Union
from discord.ext.commands.errors import ExtensionAlreadyLoaded
from discord.ext.commands.errors import ExtensionFailed
from discord.ext.commands.errors import ExtensionNotFound
from discord.ext.commands.errors import NoEntryPointError
if TYPE_CHECKING:
from tunebot.abc import BasePluginInstance
from tunebot.plugins import ServiceEvent
from bot import TuneBot
AnyDict = dict[Any, Any]
class SimplePluginManager:
_plugins: dict[str, "BasePluginInstance"] = {}
def __init__(self, bot: "TuneBot") -> None:
self.bot = bot
def get_plugin(self, name: str) -> Union["BasePluginInstance", None]:
"""
Retrieves the corresponding `BasePluginInstance` if it exists
Returns:
Union["BasePluginInstance", None]: [description]
"""
return self._plugins.get(name)
def remove_plugin(self, name: str):
"""
Unloads/Removes all components related to the `BasePluginInstance`
Args:
name (str): [description]
"""
plugin = self.get_plugin(name)
if not plugin:
return
for cog in plugin.cogs:
try:
self.bot.unload_extension(cog)
except Exception:
pass
del self._plugins[name]
def enable_plugin(self, name: str, plugin: "BasePluginInstance"):
"""
Loads/Activates all cogs/services included within the plugin
Args:
plugin_name (str): [description]
services (list[): [description]
cog_names (list[str]): [description]
"""
self._plugins[name] = plugin
for cog_name in plugin.cogs:
self.bot.load_extension(cog_name)
async def dispatch(self, event: "ServiceEvent", payload: AnyDict = {}):
"""
Dispatches an event to all registered services
Args:
event (ServiceEvent): [description]
payload (AnyDict, optional): [description]. Defaults to {}.
"""
for plugin in self._plugins.values():
for service in plugin.services:
await service.on_dispatch(event, payload)
__all__ = ("SimplePluginManager",)
+18
View File
@@ -0,0 +1,18 @@
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tunebot import ServiceBase
AnyDict = dict[Any, Any]
@dataclass
class PluginInstance:
config: AnyDict
services: list["ServiceBase"]
cogs: list[str]
__all__ = ("PluginInstance",)
+6
View File
@@ -0,0 +1,6 @@
# noreorder
from tunebot.redis.entity import *
from tunebot.redis.utils import *
from tunebot.redis.autojoin import *
from tunebot.redis.playlist import *
from tunebot.redis.playlist_source import *
+41
View File
@@ -0,0 +1,41 @@
from tunebot import AutoJoin
from tunebot import GlobalAutoJoin
from tunebot.redis import RedisBotEntity
from tunebot.redis import RedisContextEntity
class GlobalRedisAutoJoin(RedisBotEntity, GlobalAutoJoin):
async def fetch_channels(self) -> dict[str, list[str]]:
"""
Retrieves all guilds (with their configurations) where AutoJoin is enabled
"""
channels: dict[str, str] = await self.redis.hgetall(self.key("autojoin"))
return {key: value.split(":") for key, value in channels.items()}
class RedisAutoJoin(RedisContextEntity, AutoJoin):
async def update(self, voice_channel_id: int, text_channel_id: int):
"""
Upserts the configuration of an AutoJoin guild.
Args:
voice_channel_id (int): [description]
text_channel_id (int): [description]
"""
if not self.ctx.guild:
raise Exception("This method can only be invoked inside of a guild.")
value = f"{voice_channel_id}:{text_channel_id}"
await self.redis.hset(self.key("autojoin"), self.ctx.guild.id, value)
async def disable(self):
"""
Removes the AutoJoin configuration of a guild.
"""
if not self.ctx.guild:
raise Exception("This method can only be invoked inside of a guild.")
await self.redis.hdel(self.key("autojoin"), self.ctx.guild.id)
__all__ = ("GlobalRedisAutoJoin", "RedisAutoJoin")
+49
View File
@@ -0,0 +1,49 @@
from abc import ABC
from abc import abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from context import CustomContext
from aioredis.client import Redis
class RedisEntity(ABC):
@property
@abstractmethod
def redis() -> Redis:
pass
@abstractmethod
def key(self, name: str) -> str:
pass
class RedisBotEntity(RedisEntity):
def __init__(self, redis: Redis, prefix: str) -> None:
self._redis = redis
self.prefix = prefix
super().__init__()
@property
def redis(self) -> Redis:
return self._redis
def key(self, name: str) -> str:
return ":".join([self.prefix, name])
class RedisContextEntity(RedisEntity):
def __init__(self, ctx: "CustomContext") -> None:
self.ctx = ctx
super().__init__()
@property
def redis(self) -> Redis:
return self.ctx.redis
def key(self, name: str) -> str:
return ":".join([self.ctx.bot.redis_prefix, name])
__all__ = ("RedisEntity", "RedisBotEntity", "RedisContextEntity")
+42
View File
@@ -0,0 +1,42 @@
from typing import Optional
from tunebot.abc import GlobalPlaylist
from tunebot.redis import RedisBotEntity
class GlobalRedisPlaylist(RedisBotEntity, GlobalPlaylist):
async def pick_random(self, amount: Optional[int] = 1) -> set[str]:
"""
Picks an amount of random tracks from the playlist in Redis
Args:
amount (Optional[int], optional): [description]. Defaults to 1.
"""
return await self.redis.srandmember(self.key("playlist"), amount)
async def add_tracks(self, urls: list[str]):
"""
Adds one or more urls to the playlist in Redis
Args:
urls (list[str]): [description]
"""
await self.redis.sadd(self.key("playlist"), *urls)
async def clear(self):
"""
Clears the entire playlist in Redis
"""
await self.redis.delete(self.key("playlist"))
async def remove_tracks(self, track_urls: list[str]):
"""
Removes a single track from the playlist
Args:
track_url (str): [description]
"""
await self.redis.srem(self.key("playlist"), *track_urls)
__all__ = ("GlobalRedisPlaylist",)
+35
View File
@@ -0,0 +1,35 @@
from tunebot import GlobalPlaylistSource
from tunebot import PlaylistSource
from tunebot.redis import RedisBotEntity
from tunebot.redis import RedisContextEntity
class GlobalRedisPlaylistSource(RedisBotEntity, GlobalPlaylistSource):
async def fetch_sources(self) -> set[str]:
"""
Fetches all sources
"""
return await self.redis.smembers(self.key("sources"))
class RedisPlaylistSource(RedisContextEntity, PlaylistSource):
async def add(self, source_url: str):
"""
Adds a playlist source to Redis
Args:
source_url (str): [description]
"""
await self.redis.sadd(self.key("sources"), source_url)
async def remove(self, source_url: str) -> bool:
"""
Removes a playlist source from Redis
Args:
source_url (str): [description]
"""
return await self.redis.srem(self.key("sources"), source_url)
__all__ = ("GlobalRedisPlaylistSource", "RedisPlaylistSource")
+18
View File
@@ -0,0 +1,18 @@
from typing import Any
from tunebot.redis import RedisBotEntity
class GlobalRedisUtils(RedisBotEntity):
async def raw_table_lookup(self, table_name: str, keys: list[Any]) -> list[Any]:
if len(keys) == 0:
return []
result: list[Any] = await self.redis.hmget(table_name, keys)
return result
async def raw_table_del_entry(self, table_name: str, keys: list[Any]):
if len(keys) == 0:
return
await self.redis.hdel(table_name, *keys)
+3 -2
View File
@@ -1,8 +1,9 @@
from typing import Optional, Union
from typing import Optional
from typing import Union
import discord
from discord.ext.commands import Context
from discord import Embed
from discord.ext.commands import Context
class EmbedGenerator:
+21 -2
View File
@@ -1,13 +1,32 @@
from typing import Dict
from typing import TYPE_CHECKING
from discord.ext.commands import Cog
from bot import ChristmasBot
if TYPE_CHECKING:
from tunebot import BasePluginInstance
from bot import TuneBot
class BaseCog(Cog):
def __init__(self, bot: ChristmasBot) -> None:
def __init__(self, bot: "TuneBot") -> None:
self.bot = bot
slash_descriptions: Dict[str, str] = self.bot.config["slash_descriptions"]
for command in self.walk_commands():
if brief := slash_descriptions.get(command.qualified_name):
command.brief = brief
def is_lavalink_ready(self) -> bool:
return (
hasattr(self.bot, "lavalink")
and len(self.bot.lavalink.node_manager.available_nodes) > 0
)
class PluginCog(BaseCog):
def get_plugin_instance(self, name: str) -> "BasePluginInstance":
if plugin := self.bot.plugin_manager.get_plugin(name):
return plugin
raise KeyError(f"Failed to retrieve plugin instance with name: {name}")
-35
View File
@@ -1,35 +0,0 @@
from typing import Dict
from typing import List
from typing import Optional
from aioredis import Redis
from bot import redis_prefix
class AutoJoin:
@staticmethod
async def get_channels(redis: Redis) -> Dict[str, str]:
# return all channels
channels = await redis.hgetall(f"{redis_prefix}:autojoin")
return {key: value.split("-") for key, value in channels.items()}
@staticmethod
async def update_channel(
redis: Redis, guild_id: int, voice_channel_id: int, text_channel_id: int
):
await redis.hset(
f"{redis_prefix}:autojoin",
guild_id,
f"{voice_channel_id}-{text_channel_id}",
)
@staticmethod
async def del_channel(redis: Redis, guild_id: int):
await redis.hdel(f"{redis_prefix}:autojoin", guild_id)
class Playlist:
@staticmethod
async def random(redis: Redis, amount: Optional[int] = 1) -> List[str]:
return await redis.srandmember(f"{redis_prefix}:playlist", amount)
+27
View File
@@ -0,0 +1,27 @@
from typing import Callable
from typing import TYPE_CHECKING
from typing import TypeVar
from discord.ext.commands import check
from discord.ext.commands.errors import NotOwner
if TYPE_CHECKING:
from context import CustomContext
T = TypeVar("T")
def source_manager_only() -> Callable[[T], T]:
"""
A :func:`.check` that checks if the person invoking this command is allowed to modify the radio sources.
"""
async def predicate(ctx: "CustomContext") -> bool:
is_manager = ctx.author.id in ctx.bot.config["manager_ids"]
is_owner = await ctx.bot.is_owner(ctx.author)
if not is_manager and not is_owner:
raise NotOwner("You are not allowed to modify the sources.")
return True
return check(predicate)
+2 -1
View File
@@ -1,7 +1,8 @@
from discord.embeds import Embed
from context import CustomContext
from discord.ext.commands import CommandError
from context import CustomContext
class EmbeddedCommandException(CommandError):
def __init__(self, embed: Embed) -> None: