Plex-Bot-Music/PlexBot/bot.py
Joshua Arulsamy ed1ec214ee ♻️ Major revamp for queuing
Queues now use a proper asyncronous callback.
This allows for a much more consistent and error free experience.
2020-08-04 18:07:29 -06:00

193 lines
5.6 KiB
Python

import asyncio
import io
import logging
from urllib.request import urlopen
import discord
from discord import FFmpegPCMAudio
from discord.ext import commands
from discord.ext.commands import command
from fuzzywuzzy import fuzz
from plexapi.exceptions import Unauthorized
from plexapi.server import PlexServer
logger = logging.getLogger("PlexBot")
class General(commands.Cog):
def __init__(self, bot):
self.bot = bot
@command()
async def kill(self, ctx):
await ctx.send(f"Stopping upon the request of {ctx.author.mention}")
await self.bot.close()
logger.info(f"Stopping upon the request of {ctx.author.mention}")
class Plex(commands.Cog):
def __init__(self, bot, base_url, plex_token, lib_name, bot_prefix) -> None:
self.bot = bot
self.base_url = base_url
self.plex_token = plex_token
self.library_name = lib_name
self.bot_prefix = bot_prefix
try:
self.pms = PlexServer(self.base_url, self.plex_token)
except Unauthorized:
logger.fatal("Invalid Plex token, stopping...")
raise Unauthorized("Invalid Plex token")
self.music = self.pms.library.section(self.library_name)
self.vc = None
self.current_track = None
self.play_queue = asyncio.Queue()
self.play_next_event = asyncio.Event()
self.bot.loop.create_task(self._audio_player_task())
logger.info("Started bot successfully")
def _search_tracks(self, title):
tracks = self.music.searchTracks()
score = [None, -1]
for i in tracks:
s = fuzz.ratio(title.lower(), i.title.lower())
if s > score[1]:
score[0] = i
score[1] = s
elif s == score[1]:
score[0] = i
return score[0]
@command()
async def hello(self, ctx, *, member: discord.member = None):
member = member or ctx.author
await ctx.send(f"Hello {member}")
async def _play(self):
track_url = self.current_track.getStreamURL()
audio_stream = FFmpegPCMAudio(track_url)
self.vc.play(audio_stream, after=self._toggle_next)
logger.debug(f"Playing {self.current_track.title}")
logger.debug(f"URL: {track_url}")
embed, f = self._build_embed(self.current_track)
await self.ctx.send(embed=embed, file=f)
async def _audio_player_task(self):
while True:
self.play_next_event.clear()
self.current_track = await self.play_queue.get()
await self._play()
await self.play_next_event.wait()
def _toggle_next(self, error=None):
self.bot.loop.call_soon_threadsafe(self.play_next_event.set)
def _build_embed(self, track, t="play"):
"""Creates a pretty embed card.
"""
# Grab the relevant thumbnail
img_stream = urlopen(track.thumbUrl)
img = io.BytesIO(img_stream.read())
# Attach to discord embed
f = discord.File(img, filename="image0.png")
if t == "play":
title = f"Now Playing - {track.title}"
elif t == "queue":
title = f"Added to queue - {track.title}"
else:
raise ValueError(f"Unsupported type of embed {t}")
descrip = f"{track.album().title} - {track.artist().title}"
# Build the actual embed
embed = discord.Embed(
title=title, description=descrip, colour=discord.Color.red()
)
embed.set_author(name="Plex")
# Point to file attached with ctx object.
embed.set_thumbnail(url="attachment://image0.png")
return embed, f
@command()
async def play(self, ctx, *args):
if not len(args):
await ctx.send(f"Usage: {self.bot_prefix}play TITLE_OF_SONG")
return
title = " ".join(args)
track = self._search_tracks(title)
# Fail if song title can't be found
if not track:
await ctx.send(f"Can't find song: {title}")
return
# Fail if user not in vc
elif not ctx.author.voice:
await ctx.send("Join a voice channel first!")
return
# Connect to voice if not already
if not self.vc:
self.vc = await ctx.author.voice.channel.connect()
logger.debug("Connected to vc.")
# Specific add to queue message
if self.vc.is_playing():
embed, f = self._build_embed(track, t="queue")
await ctx.send(embed=embed, file=f)
# Save the context to use with async callbacks
self.ctx = ctx
# Add the song to the async queue
await self.play_queue.put(track)
@command()
async def stop(self, ctx):
if self.vc:
self.vc.stop()
await self.vc.disconnect()
self.vc = None
await ctx.send(":stop: Stopped")
@command()
async def pause(self, ctx):
if self.vc:
self.vc.pause()
await ctx.send(":play_pause: Paused")
@command()
async def resume(self, ctx):
if self.vc:
self.vc.resume()
await ctx.send(":play_pause: Resumed")
@command()
async def skip(self, ctx):
logger.debug("Skip")
if self.vc:
self.vc.stop()
self._toggle_next()
@command()
async def np(self, ctx):
if self.current_track:
embed, f = self._build_embed(self.current_track)
await ctx.send(embed=embed, file=f)
@command()
async def clear(self, ctx):
self.play_queue = asyncio.Queue()
await ctx.send(":boom: Queue cleared.")