mirror of
https://github.com/Matthww/TuneBot.git
synced 2026-09-21 22:57:48 +00:00
Merge pull request #44 from strNophix/lastfm-scrobbler
Plugins + lastfm scrobbler
This commit is contained in:
@@ -22,51 +22,74 @@ from discord.ext.commands.errors import ExtensionNotFound
|
|||||||
from discord.ext.commands.errors import NoEntryPointError
|
from discord.ext.commands.errors import NoEntryPointError
|
||||||
|
|
||||||
from context import CustomContext
|
from context import CustomContext
|
||||||
|
from tunebot.plugins import FileSystemPluginLoader
|
||||||
|
from tunebot.plugins import SimplePluginManager
|
||||||
from tunebot.redis import GlobalRedisAutoJoin
|
from tunebot.redis import GlobalRedisAutoJoin
|
||||||
from tunebot.redis import GlobalRedisPlaylist
|
from tunebot.redis import GlobalRedisPlaylist
|
||||||
from tunebot.redis import GlobalRedisPlaylistSource
|
from tunebot.redis import GlobalRedisPlaylistSource
|
||||||
|
from tunebot.redis import GlobalRedisUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from tunebot import PluginManagerBase
|
||||||
from tunebot import GlobalPlaylist
|
from tunebot import GlobalPlaylist
|
||||||
from tunebot import GlobalPlaylistSource
|
from tunebot import GlobalPlaylistSource
|
||||||
from tunebot import GlobalAutoJoin
|
from tunebot import GlobalAutoJoin
|
||||||
|
from tunebot import PluginLoaderBase
|
||||||
|
from tunebot import PluginManagerBase
|
||||||
|
from tunebot import GlobalUtils
|
||||||
|
|
||||||
|
ColorDict = dict[str, "Color"]
|
||||||
|
|
||||||
|
|
||||||
class TuneBot(commands.Bot):
|
class TuneBot(commands.Bot):
|
||||||
lavalink: lavalink.Client
|
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]):
|
def __init__(self, config: Dict[Any, Any]):
|
||||||
intents = discord.Intents(
|
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.rpc_is_help_message = True
|
||||||
self.update_status.start()
|
self.update_status.start()
|
||||||
|
|
||||||
self.config = config
|
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_prefix = self.config["redis_prefix"]
|
||||||
self._redis_client: Redis = aioredis.from_url(
|
self._redis_client: Redis = aioredis.from_url(
|
||||||
self.config["redis_url"], encoding="utf-8", decode_responses=True
|
self.config["redis_url"], encoding="utf-8", decode_responses=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.global_autojoin: GlobalAutoJoin = GlobalRedisAutoJoin(
|
self.global_autojoin = GlobalRedisAutoJoin(
|
||||||
self._redis_client,
|
self._redis_client, self.redis_prefix
|
||||||
self.redis_prefix,
|
|
||||||
)
|
)
|
||||||
self.global_playlist: GlobalPlaylist = GlobalRedisPlaylist(
|
self.global_playlist = GlobalRedisPlaylist(
|
||||||
self._redis_client,
|
self._redis_client, self.redis_prefix
|
||||||
self.redis_prefix,
|
|
||||||
)
|
)
|
||||||
self.global_playlist_source: GlobalPlaylistSource = GlobalRedisPlaylistSource(
|
self.global_playlist_source = GlobalRedisPlaylistSource(
|
||||||
self._redis_client,
|
self._redis_client, self.redis_prefix
|
||||||
self.redis_prefix,
|
|
||||||
)
|
)
|
||||||
|
self.global_utils = GlobalRedisUtils(self._redis_client, self.redis_prefix)
|
||||||
|
|
||||||
self.invite_link: str = ""
|
self.plugin_loader = FileSystemPluginLoader(self)
|
||||||
|
self.plugin_manager = SimplePluginManager(self)
|
||||||
|
|
||||||
slash_guilds = None
|
slash_guilds = None
|
||||||
if len(self.config["slash_command_guilds"]) > 0:
|
if len(self.config["slash_command_guilds"]) > 0:
|
||||||
@@ -86,23 +109,24 @@ class TuneBot(commands.Bot):
|
|||||||
self.loop.create_task(self.async_init())
|
self.loop.create_task(self.async_init())
|
||||||
|
|
||||||
async def async_init(self):
|
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]:
|
async def prefix_callable(self, _, msg: Message) -> List[str]:
|
||||||
return commands.when_mentioned_or(*self.config["prefixes"])(self, msg)
|
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:
|
for cog in cog_names:
|
||||||
try:
|
try:
|
||||||
self.load_extension(cog)
|
self.load_extension(cog)
|
||||||
print(f"Succesfully loaded extension {cog}.")
|
print(f"[✓] loaded extension: {cog}.")
|
||||||
except (
|
except (
|
||||||
ExtensionNotFound,
|
ExtensionNotFound,
|
||||||
ExtensionAlreadyLoaded,
|
ExtensionAlreadyLoaded,
|
||||||
NoEntryPointError,
|
NoEntryPointError,
|
||||||
ExtensionFailed,
|
ExtensionFailed,
|
||||||
) as e:
|
) 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):
|
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"
|
self.invite_link = f"https://discord.com/oauth2/authorize?client_id={self.user.id}&permissions=3230720&scope=bot%20applications.commands"
|
||||||
@@ -117,7 +141,7 @@ class TuneBot(commands.Bot):
|
|||||||
ll["host"], ll["port"], ll["password"], ll["region"], ll["name"]
|
ll["host"], ll["port"], ll["password"], ll["region"], ll["name"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_colours(self, colors: Dict[str, str]) -> Dict[str, Color]:
|
def process_colours(self, colors: Dict[str, str]) -> ColorDict:
|
||||||
colour_dict: Dict[str, Color] = {}
|
colour_dict: Dict[str, Color] = {}
|
||||||
for name, color in colors.items():
|
for name, color in colors.items():
|
||||||
colour_dict[name] = Color(int(color, 16))
|
colour_dict[name] = Color(int(color, 16))
|
||||||
@@ -126,6 +150,19 @@ class TuneBot(commands.Bot):
|
|||||||
async def get_context(self, message: Message, *, cls=CustomContext):
|
async def get_context(self, message: Message, *, cls=CustomContext):
|
||||||
return await super().get_context(message, cls=cls)
|
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)
|
@tasks.loop(seconds=30)
|
||||||
async def update_status(self):
|
async def update_status(self):
|
||||||
await self.wait_until_ready()
|
await self.wait_until_ready()
|
||||||
@@ -141,13 +178,6 @@ class TuneBot(commands.Bot):
|
|||||||
await self.change_presence(activity=activity)
|
await self.change_presence(activity=activity)
|
||||||
|
|
||||||
|
|
||||||
config_path = "config.json"
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
config_path = sys.argv[1]
|
|
||||||
|
|
||||||
config = json.load(open(config_path, "r", encoding="utf-8"))
|
|
||||||
redis_prefix = config["redis_prefix"]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
import uvloop
|
import uvloop
|
||||||
@@ -157,5 +187,10 @@ if __name__ == "__main__":
|
|||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
pass
|
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")
|
token = config.pop("token")
|
||||||
TuneBot(config).run(token, reconnect=True)
|
TuneBot(config).run(token, reconnect=True)
|
||||||
|
|||||||
+30
-2
@@ -1,24 +1,29 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import lavalink
|
import lavalink
|
||||||
from discord import Embed
|
from discord import Embed
|
||||||
from discord.channel import TextChannel
|
from discord.channel import TextChannel
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands.context import Context
|
|
||||||
from discord.ext.commands.errors import CommandError
|
from discord.ext.commands.errors import CommandError
|
||||||
from lavalink.models import AudioTrack
|
from lavalink.models import AudioTrack
|
||||||
from lavalink.models import DefaultPlayer
|
from lavalink.models import DefaultPlayer
|
||||||
|
|
||||||
from bot import TuneBot
|
from bot import TuneBot
|
||||||
from context import CustomContext
|
from context import CustomContext
|
||||||
|
from tunebot.plugins import ServiceEvent
|
||||||
from utils.classes import BaseCog
|
from utils.classes import BaseCog
|
||||||
from utils.EmbedGenerator import EmbedGenerator
|
from utils.EmbedGenerator import EmbedGenerator
|
||||||
from utils.exceptions import EmbeddedCommandException
|
from utils.exceptions import EmbeddedCommandException
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from discord import VoiceChannel
|
||||||
|
|
||||||
url_rx = re.compile(r"https?://(?:www\.)?.+")
|
url_rx = re.compile(r"https?://(?:www\.)?.+")
|
||||||
|
|
||||||
|
|
||||||
@@ -217,13 +222,36 @@ class Music(BaseCog):
|
|||||||
guild = self.bot.get_guild(guild_id)
|
guild = self.bot.get_guild(guild_id)
|
||||||
await guild.voice_client.disconnect(force=True)
|
await guild.voice_client.disconnect(force=True)
|
||||||
elif isinstance(event, lavalink.events.TrackStartEvent):
|
elif isinstance(event, lavalink.events.TrackStartEvent):
|
||||||
channel_id = int(event.player.fetch("channel"))
|
if channel := event.player.fetch("channel"):
|
||||||
|
channel_id = int(channel)
|
||||||
channel: TextChannel = self.bot.get_channel(channel_id)
|
channel: TextChannel = self.bot.get_channel(channel_id)
|
||||||
embed = await self.create_track_embed(event.player.current)
|
embed = await self.create_track_embed(event.player.current)
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
elif isinstance(event, lavalink.events.TrackEndEvent):
|
elif isinstance(event, lavalink.events.TrackEndEvent):
|
||||||
await self.fill_player_queue(event.player, 1)
|
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"])
|
@commands.command(name="connect", aliases=["p", "play", "join"])
|
||||||
async def play(self, ctx: CustomContext):
|
async def play(self, ctx: CustomContext):
|
||||||
"""Start the radio"""
|
"""Start the radio"""
|
||||||
|
|||||||
+14
-1
@@ -22,5 +22,18 @@
|
|||||||
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"],
|
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"],
|
||||||
"slash_command_guilds": [],
|
"slash_command_guilds": [],
|
||||||
"queue_buffer_size": 5,
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Any
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from aioredis.client import Redis
|
from aioredis.client import Redis
|
||||||
@@ -14,12 +15,18 @@ if TYPE_CHECKING:
|
|||||||
from tunebot import PlaylistSource
|
from tunebot import PlaylistSource
|
||||||
from tunebot import AutoJoin
|
from tunebot import AutoJoin
|
||||||
from utils.classes import BaseCog
|
from utils.classes import BaseCog
|
||||||
|
from tunebot.plugins import ServiceEvent
|
||||||
|
|
||||||
|
AnyDict = dict[Any, Any]
|
||||||
|
|
||||||
|
|
||||||
class CustomContext(commands.Context):
|
class CustomContext(commands.Context):
|
||||||
bot: "TuneBot"
|
bot: "TuneBot"
|
||||||
cog: "BaseCog"
|
cog: "BaseCog"
|
||||||
|
|
||||||
|
async def dispatch(self, event: "ServiceEvent", payload: AnyDict):
|
||||||
|
await self.bot.plugin_manager.dispatch(event, payload)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def redis(self) -> Redis:
|
def redis(self) -> Redis:
|
||||||
return self.bot._redis_client
|
return self.bot._redis_client
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from typing import Any
|
||||||
from typing import Optional
|
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):
|
class GlobalPlaylistSource(ABC):
|
||||||
@@ -53,10 +63,53 @@ class AutoJoin(ABC):
|
|||||||
pass
|
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__ = (
|
__all__ = (
|
||||||
"GlobalPlaylistSource",
|
"GlobalPlaylistSource",
|
||||||
"PlaylistSource",
|
"PlaylistSource",
|
||||||
"GlobalPlaylist",
|
"GlobalPlaylist",
|
||||||
"GlobalAutoJoin",
|
"GlobalAutoJoin",
|
||||||
"AutoJoin",
|
"AutoJoin",
|
||||||
|
"ServiceBase",
|
||||||
|
"PluginManagerBase",
|
||||||
|
"PluginLoaderBase",
|
||||||
|
"GlobalUtils",
|
||||||
|
"BasePluginInstance",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 *
|
||||||
@@ -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",)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class PluginInitFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("PluginInitFailed",)
|
||||||
@@ -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",)
|
||||||
@@ -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",)
|
||||||
@@ -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",)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from tunebot.redis.entity import * # noreorder
|
# noreorder
|
||||||
|
from tunebot.redis.entity import *
|
||||||
|
from tunebot.redis.utils import *
|
||||||
from tunebot.redis.autojoin import *
|
from tunebot.redis.autojoin import *
|
||||||
from tunebot.redis.playlist import *
|
from tunebot.redis.playlist import *
|
||||||
from tunebot.redis.playlist_source import *
|
from tunebot.redis.playlist_source import *
|
||||||
|
|||||||
@@ -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)
|
||||||
+13
-3
@@ -1,13 +1,15 @@
|
|||||||
import asyncio
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
from bot import TuneBot
|
if TYPE_CHECKING:
|
||||||
|
from tunebot import BasePluginInstance
|
||||||
|
from bot import TuneBot
|
||||||
|
|
||||||
|
|
||||||
class BaseCog(Cog):
|
class BaseCog(Cog):
|
||||||
def __init__(self, bot: TuneBot) -> None:
|
def __init__(self, bot: "TuneBot") -> None:
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
slash_descriptions: Dict[str, str] = self.bot.config["slash_descriptions"]
|
slash_descriptions: Dict[str, str] = self.bot.config["slash_descriptions"]
|
||||||
@@ -20,3 +22,11 @@ class BaseCog(Cog):
|
|||||||
hasattr(self.bot, "lavalink")
|
hasattr(self.bot, "lavalink")
|
||||||
and len(self.bot.lavalink.node_manager.available_nodes) > 0
|
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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user