Compare commits

...

30 Commits

Author SHA1 Message Date
Joshua Arulsamy
360416bb02 Merge branch 'master' of github.com:jarulsamy/Plex-Bot 2022-08-03 16:45:12 -06:00
Carsten Burgard
ccee843a36 added functionality to inspect current queue and skip multiple tracks at once 2022-08-03 22:40:54 +00:00
Carsten Burgard
90c40fb0d6 rewrote looping 2022-04-15 16:11:44 +02:00
Carsten Burgard
8b90fb8e34 added debug message 2022-04-15 16:06:22 +02:00
Carsten Burgard
60ef54d8e4 python version screwup workaround 2022-04-15 15:53:30 +02:00
Carsten Burgard
280cbaed92 added basic functionality for looping queues 2022-04-15 15:41:50 +02:00
Carsten Burgard
2f17494ebd added documentation 2022-04-15 15:07:17 +02:00
Carsten Burgard
ed9e677108 playlist shuffling implemented 2022-04-15 15:05:37 +02:00
Carsten Burgard
aa51cc4301 bugfix for missing thumbnail 2022-04-15 14:27:45 +02:00
Carsten Burgard
069d88fd1e implemented looping of tracks 2022-04-15 12:43:55 +02:00
Carsten Burgard
15b7e0f50e catching a few exceptions and handling them accordingly 2022-04-15 12:42:49 +02:00
Carsten Burgard
04da5dfd42 added functionality to inspect current queue and skip multiple tracks at once 2022-04-15 11:53:43 +02:00
Joshua Arulsamy
19360e3101
Merge pull request #27 from cburgard/playlist-magic
Added command to show available playlists
2022-02-22 07:30:23 -07:00
Carsten Burgard
52099fa794 added option to show available playlists 2022-02-13 13:12:35 +01:00
Carsten Burgard
139ce07d24 fix bug related to empty playlists 2022-02-13 13:10:59 +01:00
Carsten Burgard
5f9c83b75e added documentation for missing option 2022-02-13 13:03:07 +01:00
Carsten Burgard
f8e5af87b6 allow executing outside of docker env by checking if user is root user for config path 2022-02-13 12:04:34 +01:00
Carsten Burgard
a45ccb657e make dependency on lyricsgenius optional by moving import 2022-02-13 12:02:49 +01:00
Carsten Burgard
bcbce98d91 allow lyrics section in yml file to be missing 2022-02-13 12:02:15 +01:00
Joshua Arulsamy
6521e7e26c
Merge pull request #25 from Raventhicc/patch-1
Added Lyrics in docker-compose.yml and slightly changed instructions
2021-11-28 14:54:32 -07:00
Raventric
add1a1af0d
Added Lyrics in docker-compose.yml and slightly changed instructions
This was just done to make it easier for people to understand since some people stumbled on this part.
2021-11-28 20:59:45 +05:30
Joshua Arulsamy
8f05f5e27f
Merge pull request #19 from profesaurus/feature_playlists
Feature playlists
2021-05-30 01:33:09 -06:00
profesaurus
eeb430c016 Fixed even more Codacy Static Code Analysis issues. 2021-05-27 15:39:50 -07:00
profesaurus
1ffa5a7229 Attempt to fix additional Codacy Static Code Analysis issues. 2021-05-27 15:35:34 -07:00
profesaurus
f4cd675502 Attempting to fix the Codacy Static Code Analysis issues. 2021-05-27 14:33:24 -07:00
profesaurus
921dfc02b8 Updated README.md to include the playlist command. Added the playlist command to bot.py comments. 2021-05-27 08:11:02 -07:00
profesaurus
9fb091e9e1 Added the ability to play a playlist with a new "playlist" command.
Ex. ?playlist MyPlaylistName
2021-05-26 09:28:57 -07:00
Joshua Arulsamy
efdd604d65
Merge pull request #14 from jarulsamy/hotfix
Hotfix
2020-09-23 22:46:04 -06:00
Joshua Arulsamy
7d6060ed15 🔖 Bump version 2020-09-23 22:40:20 -06:00
Joshua Arulsamy
d6f5174d20 🐛 Bump discord version
Remedies a seemingly random socketio error:

```
    future: <Task finished coro=<VoiceClient._create_socket() done, defined at /usr/local/lib/python3.7/site-packages/discord/voice_client.py:172>
    exception=gaierror(-2, 'Name or service not known')>

    Traceback (most recent call last):
    File "/usr/local/lib/python3.7/site-packages/discord/voice_client.py", line 191, in _create_socket
    self.endpoint_ip = socket.gethostbyname(self.endpoint) socket.gaierror: [Errno -2] Name or service not known
```
2020-09-23 22:36:47 -06:00
6 changed files with 437 additions and 61 deletions

View File

@ -1,8 +1,6 @@
""" """
Plex music bot for discord. Plex music bot for discord.
Do not import this module, it is intended to be
used exclusively within a docker environment.
""" """
import logging import logging
import sys import sys
@ -19,7 +17,7 @@ plex_log = logging.getLogger("Plex")
bot_log = logging.getLogger("Bot") bot_log = logging.getLogger("Bot")
def load_config(filename: str) -> Dict[str, str]: def load_config(basedir: str,filename: str) -> Dict[str, str]:
"""Loads config from yaml file """Loads config from yaml file
Grabs key/value config pairs from a file. Grabs key/value config pairs from a file.
@ -35,12 +33,12 @@ def load_config(filename: str) -> Dict[str, str]:
""" """
# All config files should be in /config # All config files should be in /config
# for docker deployment. # for docker deployment.
filename = Path("/config", filename) filename = Path(basedir, filename)
try: try:
with open(filename, "r") as config_file: with open(filename, "r") as config_file:
config = yaml.safe_load(config_file) config = yaml.safe_load(config_file)
except FileNotFoundError: except FileNotFoundError:
root_log.fatal("Configuration file not found.") root_log.fatal("Configuration file not found at '"+str(filename)+"'.")
sys.exit(-1) sys.exit(-1)
# Convert str level type to logging constant # Convert str level type to logging constant
@ -56,7 +54,7 @@ def load_config(filename: str) -> Dict[str, str]:
config["plex"]["log_level"] = levels[config["plex"]["log_level"].upper()] config["plex"]["log_level"] = levels[config["plex"]["log_level"].upper()]
config["discord"]["log_level"] = levels[config["discord"]["log_level"].upper()] config["discord"]["log_level"] = levels[config["discord"]["log_level"].upper()]
if config["lyrics"]["token"].lower() == "none": if config["lyrics"] and config["lyrics"]["token"].lower() == "none":
config["lyrics"]["token"] = None config["lyrics"] = None
return config return config

View File

@ -11,7 +11,11 @@ from .bot import General
from .bot import Plex from .bot import Plex
# Load config from file # Load config from file
config = load_config("config.yaml") configdir = "config"
from os import geteuid
if geteuid() == 0:
configdir = "/config"
config = load_config(configdir,"config.yaml")
BOT_PREFIX = config["discord"]["prefix"] BOT_PREFIX = config["discord"]["prefix"]
TOKEN = config["discord"]["token"] TOKEN = config["discord"]["token"]
@ -20,7 +24,10 @@ BASE_URL = config["plex"]["base_url"]
PLEX_TOKEN = config["plex"]["token"] PLEX_TOKEN = config["plex"]["token"]
LIBRARY_NAME = config["plex"]["library_name"] LIBRARY_NAME = config["plex"]["library_name"]
LYRICS_TOKEN = config["lyrics"]["token"] if config["lyrics"]:
LYRICS_TOKEN = config["lyrics"]["token"]
else:
LYRICS_TOKEN = None
# Set appropiate log level # Set appropiate log level
root_log = logging.getLogger() root_log = logging.getLogger()

View File

@ -1,5 +1,5 @@
"""Track version number of package.""" """Track version number of package."""
VERSION = "1.0.2" VERSION = "1.0.3"
if __name__ == "__main__": if __name__ == "__main__":
print(VERSION) print(VERSION)

View File

@ -3,14 +3,15 @@ import asyncio
import io import io
import logging import logging
from urllib.request import urlopen from urllib.request import urlopen
import requests
import discord import discord
import lyricsgenius
from async_timeout import timeout from async_timeout import timeout
from discord import FFmpegPCMAudio from discord import FFmpegPCMAudio
from discord.ext import commands from discord.ext import commands
from discord.ext.commands import command from discord.ext.commands import command
from plexapi.exceptions import Unauthorized from plexapi.exceptions import Unauthorized
from plexapi.exceptions import NotFound
from plexapi.server import PlexServer from plexapi.server import PlexServer
from .exceptions import MediaNotFoundError from .exceptions import MediaNotFoundError
@ -29,11 +30,19 @@ General:
Plex: Plex:
play <SONG_NAME> - Play a song from the plex server. play <SONG_NAME> - Play a song from the plex server.
album <ALBUM_NAME> - Queue an entire album to play. album <ALBUM_NAME> - Queue an entire album to play.
playlist <PLAYLIST_NAME> - Queue an entire playlist to play.
show_playlists <ARG> <ARG> - Query for playlists with a name matching any of the arguments.
lyrics - Print the lyrics of the song (Requires Genius API) lyrics - Print the lyrics of the song (Requires Genius API)
np - Print the current playing song. np - Print the current playing song.
q - Print the current queue (This can take very long!)
stop - Halt playback and leave vc. stop - Halt playback and leave vc.
loop - Loop the current song.
loopq - Loop the current queue.
unloop - Disable looping the current track.
unloopq - Disable looping the current queue.
pause - Pause playback. pause - Pause playback.
resume - Resume playback. resume - Resume playback.
skip - Skip the current song. Give a number as argument to skip more than 1.
clear - Clear play queue. clear - Clear play queue.
[] - Optional args. [] - Optional args.
@ -41,13 +50,15 @@ Plex:
class General(commands.Cog): class General(commands.Cog):
"""General commands """
General commands
Manage general bot behavior Manage general bot behavior
""" """
def __init__(self, bot): def __init__(self, bot):
"""Initialize commands """
Initialize commands
Args: Args:
bot: discord.ext.command.Bot, bind for cogs bot: discord.ext.command.Bot, bind for cogs
@ -62,7 +73,8 @@ class General(commands.Cog):
@command() @command()
async def kill(self, ctx, *args): async def kill(self, ctx, *args):
"""Kill the bot """
Kill the bot
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -82,7 +94,8 @@ class General(commands.Cog):
@command(name="help") @command(name="help")
async def help(self, ctx): async def help(self, ctx):
"""Prints command help """
Prints command help
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -98,7 +111,8 @@ class General(commands.Cog):
@command() @command()
async def cleanup(self, ctx, limit=250): async def cleanup(self, ctx, limit=250):
"""Delete old messages from bot """
Delete old messages from bot
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -148,7 +162,8 @@ class Plex(commands.Cog):
# within the bot. # within the bot.
def __init__(self, bot, **kwargs): def __init__(self, bot, **kwargs):
"""Initializes Plex resources """
Initializes Plex resources
Connects to Plex library and sets up Connects to Plex library and sets up
all asyncronous communications. all asyncronous communications.
@ -173,6 +188,7 @@ class Plex(commands.Cog):
self.bot_prefix = bot.command_prefix self.bot_prefix = bot.command_prefix
if kwargs["lyrics_token"]: if kwargs["lyrics_token"]:
import lyricsgenius
self.genius = lyricsgenius.Genius(kwargs["lyrics_token"]) self.genius = lyricsgenius.Genius(kwargs["lyrics_token"])
else: else:
plex_log.warning("No lyrics token specified, lyrics disabled") plex_log.warning("No lyrics token specified, lyrics disabled")
@ -191,7 +207,10 @@ class Plex(commands.Cog):
# Initialize necessary vars # Initialize necessary vars
self.voice_channel = None self.voice_channel = None
self.current_track = None self.current_track = None
self.is_looping = False
self.loop_queue = None
self.np_message_id = None self.np_message_id = None
self.show_queue_message_ids = []
self.ctx = None self.ctx = None
# Initialize events # Initialize events
@ -202,7 +221,8 @@ class Plex(commands.Cog):
self.bot.loop.create_task(self._audio_player_task()) self.bot.loop.create_task(self._audio_player_task())
def _search_tracks(self, title: str): def _search_tracks(self, title: str):
"""Search the Plex music db for track """
Search the Plex music db for track
Args: Args:
title: str title of song to search for title: str title of song to search for
@ -220,7 +240,8 @@ class Plex(commands.Cog):
raise MediaNotFoundError("Track cannot be found") raise MediaNotFoundError("Track cannot be found")
def _search_albums(self, title: str): def _search_albums(self, title: str):
"""Search the Plex music db for album """
Search the Plex music db for album
Args: Args:
title: str title of album to search for title: str title of album to search for
@ -237,8 +258,36 @@ class Plex(commands.Cog):
except IndexError: except IndexError:
raise MediaNotFoundError("Album cannot be found") raise MediaNotFoundError("Album cannot be found")
def _search_playlists(self, title: str):
"""
Search the Plex music db for playlist
Args:
title: str title of playlist to search for
Returns:
plexapi.playlist pointing to best matching title
Raises:
MediaNotFoundError: Title of playlist can't be found in plex db
"""
try:
return self.pms.playlist(title)
except NotFound:
raise MediaNotFoundError("Playlist cannot be found")
def _get_playlists(self):
"""
Search the Plex music db for playlist
Returns:
List of plexapi.playlist
"""
return self.pms.playlists()
async def _play(self): async def _play(self):
"""Heavy lifting of playing songs """
Heavy lifting of playing songs
Grabs the appropiate streaming URL, sends the `now playing` Grabs the appropiate streaming URL, sends the `now playing`
message, and initiates playback in the vc. message, and initiates playback in the vc.
@ -255,9 +304,12 @@ class Plex(commands.Cog):
track_url = self.current_track.getStreamURL() track_url = self.current_track.getStreamURL()
audio_stream = FFmpegPCMAudio(track_url) audio_stream = FFmpegPCMAudio(track_url)
while self.voice_channel.is_playing(): while self.voice_channel and self.voice_channel.is_playing():
asyncio.sleep(2) bot_log.debug("waiting for track to finish")
await asyncio.sleep(2)
bot_log.debug("track finished")
if self.voice_channel:
self.voice_channel.play(audio_stream, after=self._toggle_next) self.voice_channel.play(audio_stream, after=self._toggle_next)
plex_log.debug("%s - URL: %s", self.current_track, track_url) plex_log.debug("%s - URL: %s", self.current_track, track_url)
@ -265,8 +317,32 @@ class Plex(commands.Cog):
embed, img = self._build_embed_track(self.current_track) embed, img = self._build_embed_track(self.current_track)
self.np_message_id = await self.ctx.send(embed=embed, file=img) self.np_message_id = await self.ctx.send(embed=embed, file=img)
async def _play_next(self):
try:
from asyncio.exceptions import CancelledError
except ImportError:
from concurrent.futures._base import CancelledError
if self.is_looping:
self.current_track = self.is_looping
else:
try:
self.current_track = await self.play_queue.get()
except CancelledError:
bot_log.debug("failed to pop queue")
if not self.current_track and self.loop_queue:
bot_log.debug("swapping loop queue and play queue")
for item in self.loop_queue:
await self.play_queue.put(item)
try:
self.current_track = await self.play_queue.get()
except CancelledError:
bot_log.debug("failed to pop queue")
async def _audio_player_task(self): async def _audio_player_task(self):
"""Coroutine to handle playback and queuing """
Coroutine to handle playback and queuing
Always-running function awaiting new songs to be added. Always-running function awaiting new songs to be added.
Auto disconnects from VC if idle for > 15 seconds. Auto disconnects from VC if idle for > 15 seconds.
@ -287,20 +363,23 @@ class Plex(commands.Cog):
try: try:
# Disconnect after 15 seconds idle # Disconnect after 15 seconds idle
async with timeout(15): async with timeout(15):
self.current_track = await self.play_queue.get() await self._play_next()
except asyncio.TimeoutError: except asyncio.TimeoutError:
bot_log("timeout - disconnecting")
await self.voice_channel.disconnect() await self.voice_channel.disconnect()
self.voice_channel = None self.voice_channel = None
if not self.current_track: if not self.current_track:
self.current_track = await self.play_queue.get() await self._play_next()
await self._play() await self._play()
await self.play_next_event.wait() await self.play_next_event.wait()
if self.np_message_id:
await self.np_message_id.delete() await self.np_message_id.delete()
def _toggle_next(self, error=None): def _toggle_next(self, error=None):
"""Callback for vc playback """
Callback for vc playback
Clears current track, then activates _audio_player_task Clears current track, then activates _audio_player_task
to play next in queue or disconnect. to play next in queue or disconnect.
@ -319,7 +398,8 @@ class Plex(commands.Cog):
@staticmethod @staticmethod
def _build_embed_track(track, type_="play"): def _build_embed_track(track, type_="play"):
"""Creates a pretty embed card for tracks """
Creates a pretty embed card for tracks
Builds a helpful status embed with the following info: Builds a helpful status embed with the following info:
Status, song title, album, artist and album art. All Status, song title, album, artist and album art. All
@ -337,16 +417,22 @@ class Plex(commands.Cog):
ValueError: Unsupported type of embed {type_} ValueError: Unsupported type of embed {type_}
""" """
# Grab the relevant thumbnail # Grab the relevant thumbnail
img_stream = urlopen(track.thumbUrl) if track.thumbUrl:
img_stream = requests.get(track.thumbUrl, stream=True).raw
img = io.BytesIO(img_stream.read()) img = io.BytesIO(img_stream.read())
# Attach to discord embed # Attach to discord embed
art_file = discord.File(img, filename="image0.png") art_file = discord.File(img, filename="image0.png")
else:
art_file = None
# Get appropiate status message # Get appropiate status message
if type_ == "play": if type_ == "play":
title = f"Now Playing - {track.title}" title = f"Now Playing - {track.title}"
elif type_ == "queue": elif type_ == "queue":
title = f"Added to queue - {track.title}" title = f"Added to queue - {track.title}"
elif type_ == "queued":
title = f"Next in line - {track.title}"
else: else:
raise ValueError(f"Unsupported type of embed {type_}") raise ValueError(f"Unsupported type of embed {type_}")
@ -367,7 +453,8 @@ class Plex(commands.Cog):
@staticmethod @staticmethod
def _build_embed_album(album): def _build_embed_album(album):
"""Creates a pretty embed card for albums """
Creates a pretty embed card for albums
Builds a helpful status embed with the following info: Builds a helpful status embed with the following info:
album, artist, and album art. All pertitent information album, artist, and album art. All pertitent information
@ -384,7 +471,7 @@ class Plex(commands.Cog):
None None
""" """
# Grab the relevant thumbnail # Grab the relevant thumbnail
img_stream = urlopen(album.thumbUrl) img_stream = requests.get(album.thumbUrl, stream=True).raw
img = io.BytesIO(img_stream.read()) img = io.BytesIO(img_stream.read())
# Attach to discord embed # Attach to discord embed
@ -401,8 +488,47 @@ class Plex(commands.Cog):
return embed, art_file return embed, art_file
@staticmethod
def _build_embed_playlist(self, playlist, title, descrip):
"""
Creates a pretty embed card for playlists
Builds a helpful status embed with the following info:
playlist art. All pertitent information
is grabbed dynamically from the Plex db.
Args:
playlist: plexapi.playlist object of playlist
Returns:
embed: discord.embed fully constructed payload.
thumb_art: io.BytesIO of playlist thumbnail img.
Raises:
None
"""
# Grab the relevant thumbnail
try:
img_stream = requests.get(self.pms.url(playlist.composite, True), stream=True).raw
img = io.BytesIO(img_stream.read())
except:
raise MediaNotFoundError("no image available")
# Attach to discord embed
art_file = discord.File(img, filename="image0.png")
embed = discord.Embed(
title=title, description=descrip, colour=discord.Color.red()
)
embed.set_author(name="Plex")
embed.set_thumbnail(url="attachment://image0.png")
bot_log.debug("Built embed for playlist - %s", playlist.title)
return embed, art_file
async def _validate(self, ctx): async def _validate(self, ctx):
"""Ensures user is in a vc """
Ensures user is in a vc
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -421,12 +547,16 @@ class Plex(commands.Cog):
# Connect to voice if not already # Connect to voice if not already
if not self.voice_channel: if not self.voice_channel:
try:
self.voice_channel = await ctx.author.voice.channel.connect() self.voice_channel = await ctx.author.voice.channel.connect()
bot_log.debug("Connected to vc.") bot_log.debug("Connected to vc.")
except asyncio.exceptions.TimeoutError:
bot_log.debug("Cannot connect to vc - timeout")
@command() @command()
async def play(self, ctx, *args): async def play(self, ctx, *args):
"""User command to play song """
User command to play song
Searchs plex db and either, initiates playback, or Searchs plex db and either, initiates playback, or
adds to queue. Handles invalid usage from the user. adds to queue. Handles invalid usage from the user.
@ -458,7 +588,7 @@ class Plex(commands.Cog):
pass pass
# Specific add to queue message # Specific add to queue message
if self.voice_channel.is_playing(): if self.voice_channel and self.voice_channel.is_playing():
bot_log.debug("Added to queue - %s", title) bot_log.debug("Added to queue - %s", title)
embed, img = self._build_embed_track(track, type_="queue") embed, img = self._build_embed_track(track, type_="queue")
await ctx.send(embed=embed, file=img) await ctx.send(embed=embed, file=img)
@ -468,7 +598,8 @@ class Plex(commands.Cog):
@command() @command()
async def album(self, ctx, *args): async def album(self, ctx, *args):
"""User command to play song """
User command to play song
Searchs plex db and either, initiates playback, or Searchs plex db and either, initiates playback, or
adds to queue. Handles invalid usage from the user. adds to queue. Handles invalid usage from the user.
@ -506,9 +637,124 @@ class Plex(commands.Cog):
for track in album.tracks(): for track in album.tracks():
await self.play_queue.put(track) await self.play_queue.put(track)
async def play_playlist(self, title, shuffle=False):
try:
playlist = self._search_playlists(title)
except MediaNotFoundError:
await self.ctx.send(f"Can't find playlist: {title}")
bot_log.debug("Failed to queue playlist, can't find - %s", title)
return
try:
await self._validate(self.ctx)
except VoiceChannelError:
pass
try:
embed, img = self._build_embed_playlist(self, playlist, "Added playlist to queue", playlist.title)
await self.ctx.send(embed=embed, file=img)
items = [ item for item in playlist.items() if item.TYPE == "track" ]
if shuffle:
from random import shuffle
shuffle(items)
for item in items:
await self.play_queue.put(item)
bot_log.debug("Added to queue - %s", title)
except MediaNotFoundError:
await self.ctx.send(message="Playlist "+title+" seems to be empty!")
bot_log.debug("Playlist empty - %s", title)
@command()
async def playlist(self, ctx, *args):
"""
User command to play playlist
Searchs plex db and either, initiates playback, or
adds to queue. Handles invalid usage from the user.
Args:
ctx: discord.ext.commands.Context message context from command
*args: Title of playlist to play
Returns:
None
Raises:
None
"""
# Save the context to use with async callbacks
self.ctx = ctx
title = " ".join(args)
await self.play_playlist(title)
@command()
async def playlist_shuffle(self, ctx, *args):
"""
User command to play playlist in shuffle mode
Searchs plex db and either, initiates playback, or
adds to queue. Handles invalid usage from the user.
Args:
ctx: discord.ext.commands.Context message context from command
*args: Title of playlist to play
Returns:
None
Raises:
None
"""
# Save the context to use with async callbacks
self.ctx = ctx
title = " ".join(args)
await self.play_playlist(title,shuffle=True)
@command()
async def show_playlists(self, ctx, *args):
"""
User command to show playlists
Searchs plex db and shows playlists matching.
Args:
ctx: discord.ext.commands.Context message context from command
*args: String filter for playlist names
Returns:
None
Raises:
None
"""
# Save the context to use with async callbacks
self.ctx = ctx
playlists = self._get_playlists()
try:
await self._validate(ctx)
except VoiceChannelError:
pass
for playlist in playlists:
if args and not any(arg in playlist.title for arg in args):
continue
from datetime import timedelta
if playlist.duration:
seconds = playlist.duration / 1000
embed, img = self._build_embed_playlist(self, playlist, playlist.title, "{:0>8}".format(str(timedelta(seconds=seconds))))
await ctx.send(embed=embed, file=img)
@command() @command()
async def stop(self, ctx): async def stop(self, ctx):
"""User command to stop playback """
User command to stop playback
Stops playback and disconnects from vc. Stops playback and disconnects from vc.
@ -529,9 +775,84 @@ class Plex(commands.Cog):
bot_log.debug("Stopped") bot_log.debug("Stopped")
await ctx.send(":stop_button: Stopped") await ctx.send(":stop_button: Stopped")
@command()
async def loop(self, ctx):
"""
User command to activate looping the current track
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
bot_log.debug("Looping "+str(self.current_track))
self.is_looping = self.current_track
@command()
async def loopq(self, ctx):
"""
User command to activate looping the current queue
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
bot_log.debug("Looping current queue")
if self.current_track:
self.loop_queue = [ self.current_track ]
else:
self.loop_queue = []
for item in self.play_queue._queue:
self.loop_queue.append(item)
@command()
async def unloop(self, ctx):
"""
User command to deactivate looping the current track
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
bot_log.debug("Looping current queue")
self.is_looping = False
@command()
async def unloopq(self, ctx):
"""
User command to deactivate looping the current queue
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
bot_log.debug("Unlooping")
self.loop_queue = None
@command() @command()
async def pause(self, ctx): async def pause(self, ctx):
"""User command to pause playback """
User command to pause playback
Pauses playback, but doesn't reset anything Pauses playback, but doesn't reset anything
to allow playback resuming. to allow playback resuming.
@ -552,7 +873,8 @@ class Plex(commands.Cog):
@command() @command()
async def resume(self, ctx): async def resume(self, ctx):
"""User command to resume playback """
User command to resume playback
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -569,8 +891,9 @@ class Plex(commands.Cog):
await ctx.send(":play_pause: Resumed") await ctx.send(":play_pause: Resumed")
@command() @command()
async def skip(self, ctx): async def skip(self, ctx, *args):
"""User command to skip song in queue """
User command to skip song in queue
Skips currently playing song. If no other songs in Skips currently playing song. If no other songs in
queue, stops playback, otherwise moves to next song. queue, stops playback, otherwise moves to next song.
@ -584,15 +907,22 @@ class Plex(commands.Cog):
Raises: Raises:
None None
""" """
bot_log.debug("Skip") n = 1
if args:
n = int(args[0])
bot_log.debug("Skipping "+str(n))
if self.voice_channel: if self.voice_channel:
self.voice_channel.stop() self.voice_channel.stop()
bot_log.debug("Skipped") bot_log.debug("Skipped")
if n>1:
for i in range(n-1):
await self.play_queue.get()
self._toggle_next() self._toggle_next()
@command(name="np") @command(name="np")
async def now_playing(self, ctx): async def now_playing(self, ctx):
"""User command to get currently playing song. """
User command to get currently playing song.
Deletes old `now playing` status message, Deletes old `now playing` status message,
Creates a new one with up to date information. Creates a new one with up to date information.
@ -607,18 +937,55 @@ class Plex(commands.Cog):
None None
""" """
if self.current_track: if self.current_track:
embed, img = self._build_embed_track(self.current_track) embed, img = self._build_embed_track(self.current_track,type_="play")
bot_log.debug("Now playing") bot_log.debug("Now playing")
if self.np_message_id: if self.np_message_id:
try:
await self.np_message_id.delete() await self.np_message_id.delete()
bot_log.debug("Deleted old np status") bot_log.debug("Deleted old np status")
except discord.errors.NotFound:
pass
bot_log.debug("Created np status") bot_log.debug("Created np status")
self.np_message_id = await ctx.send(embed=embed, file=img) self.np_message_id = await ctx.send(embed=embed, file=img)
@command(name="q")
async def show_queue(self, ctx):
"""
User command to print the current queue
Deletes old `now playing` status message,
Creates a new one with up to date information.
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
bot_log.debug("Deleted old queue messages")
for msg in self.show_queue_message_ids:
await msg.delete()
# need to do this in order to avoid errors when queue is modified during inspection
elems = []
for track in self.play_queue._queue:
elems.append(track)
for track in elems:
embed, img = self._build_embed_track(track,type_="queued")
bot_log.debug("Show queue")
bot_log.debug("Created queue message")
self.show_queue_message_ids.append(await ctx.send(embed=embed, file=img))
@command() @command()
async def clear(self, ctx): async def clear(self, ctx):
"""User command to clear play queue. """
User command to clear play queue.
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command
@ -630,12 +997,14 @@ class Plex(commands.Cog):
None None
""" """
self.play_queue = asyncio.Queue() self.play_queue = asyncio.Queue()
self.loop_queue = None
bot_log.debug("Cleared queue") bot_log.debug("Cleared queue")
await ctx.send(":boom: Queue cleared.") await ctx.send(":boom: Queue cleared.")
@command() @command()
async def lyrics(self, ctx): async def lyrics(self, ctx):
"""User command to get lyrics of a song. """
User command to get lyrics of a song.
Args: Args:
ctx: discord.ext.commands.Context message context from command ctx: discord.ext.commands.Context message context from command

View File

@ -63,6 +63,9 @@ Plex-Bot runs entirely in a Docker container. Ensure you have Docker and docker-
token: "<PLEX_TOKEN>" token: "<PLEX_TOKEN>"
library_name: "<LIBRARY_NAME>" library_name: "<LIBRARY_NAME>"
log_level: "debug" log_level: "debug"
lyrics:
token: "none" # Add your token here if you enable lyrics
``` ```
4. Create a Discord bot application: 4. Create a Discord bot application:
@ -91,8 +94,6 @@ Plex-Bot runs entirely in a Docker container. Ensure you have Docker and docker-
6. Get your Lyrics Genius token (Optional): 6. Get your Lyrics Genius token (Optional):
If you wanty to disable this feature, set token to `None` in `config/config.yaml`
If you would like to enable the lyrics feature of the bot, you need to signup for a free GeniusLyrics account, [here](https://genius.com/api-clients). If you would like to enable the lyrics feature of the bot, you need to signup for a free GeniusLyrics account, [here](https://genius.com/api-clients).
After you make an account: After you make an account:
@ -103,7 +104,7 @@ Plex-Bot runs entirely in a Docker container. Ensure you have Docker and docker-
3. Set the redirect url to: `http://localhost` 3. Set the redirect url to: `http://localhost`
4. Copy the **Client Access Token** to `config/config.yaml` 4. Copy the **Client Access Token** and replace `None` with your token in `config/config.yaml`
7. Customize remaining settings 7. Customize remaining settings
@ -137,6 +138,7 @@ General:
Plex: Plex:
play <SONG_NAME> - Play a song from the plex server. play <SONG_NAME> - Play a song from the plex server.
album <ALBUM_NAME> - Queue an entire album to play. album <ALBUM_NAME> - Queue an entire album to play.
playlist <PLAYLIST_NAME> - Queue an entire playlist to play.
lyrics - Print the lyrics of the song (Requires Genius API) lyrics - Print the lyrics of the song (Requires Genius API)
np - Print the current playing song. np - Print the current playing song.
stop - Halt playback and leave vc. stop - Halt playback and leave vc.

View File

@ -1,4 +1,4 @@
discord.py==1.3.4 discord.py==1.4.1
PlexAPI==4.0.0 PlexAPI==4.0.0
fuzzywuzzy==0.18.0 fuzzywuzzy==0.18.0
pynacl==1.4.0 pynacl==1.4.0