34 Commits

Author SHA1 Message Date
7d6060ed15 🔖 Bump version 2020-09-23 22:40:20 -06:00
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
151f650bb2 Merge pull request #13 from jarulsamy/dev
v1.0.2
2020-09-06 15:39:10 -06:00
5eceab7a22 📝 Add lyrics updates 2020-09-06 15:33:25 -06:00
545654f9a7 Add lyrics command
Optionally grab lyrics of a song using the
lyrics command. Required LyricsGenius API token.
2020-09-06 15:32:40 -06:00
57490cdf17 Merge pull request #12 from jarulsamy/dev
v1.0.1
2020-08-29 20:09:14 -06:00
a6be6eac37 Merge branch 'master' into dev 2020-08-29 20:08:38 -06:00
5ba7d751d0 👥 Add label of maintainer 2020-08-14 20:57:03 -06:00
d0b6c25359 Pinned dependencies 2020-08-13 03:21:00 -06:00
db188d968b 🎨 Static methods 2020-08-13 03:20:41 -06:00
b05dd0598b Merge pull request #11 from jarulsamy/dev
v1.0.1
2020-08-13 02:53:46 -06:00
fc4682c210 🔖 Bump version 2020-08-13 02:52:48 -06:00
228c2b480b Add custom help command 2020-08-13 02:52:22 -06:00
08a235d55e 📝 Add docstrings 2020-08-13 02:52:11 -06:00
880c4d50f1 📝 Update help 2020-08-13 02:51:34 -06:00
f12701f4c5 🐛 Fix auto restart on dev env 2020-08-13 02:47:54 -06:00
f95d5c1fd2 Add album playback features
Queue a whole album with one single command.
2020-08-13 02:27:46 -06:00
56fd4aa5ab Makefile for dev and prod envs 2020-08-13 01:58:31 -06:00
5f90b17b0e Switch to PlexAPI for search 2020-08-13 01:58:12 -06:00
cf2bc24f8a 🐛 Fix always skip push 2020-08-10 03:03:54 -06:00
1839cc5d03 Merge pull request #10 from codacy-badger/codacy-badge
Add a Codacy badge to README.md
2020-08-10 02:42:52 -06:00
e6c4b84538 Add Codacy badge 2020-08-10 08:42:13 +00:00
c1cba637b8 Merge pull request #9 from jarulsamy/dev
v1.0.0
2020-08-10 02:37:42 -06:00
7f49c4d958 🔖 Tag release 2020-08-10 02:33:15 -06:00
ed1a64cb52 🎨 Fix style of docstrings 2020-08-10 02:26:38 -06:00
7471da85f7 Seperate field for pushing package 2020-08-10 02:25:48 -06:00
4633247004 🐛 Cleaner builds, allow some compilation 2020-08-10 02:25:33 -06:00
98a36c6cbc 🐛 Update to support shell scripts for deploy 2020-08-10 02:18:06 -06:00
b7ab589f6e 🎨 Switch to shell scripts for deployment 2020-08-10 02:16:11 -06:00
4aadd886d5 📝 Format fixes 2020-08-09 20:12:31 -06:00
3e90c9e9ef Merge pull request #8 from jarulsamy/dev
Docs Overhaul
2020-08-09 15:26:54 -06:00
33bbf21bab Merge branch 'master' into dev 2020-08-09 15:26:01 -06:00
253f2a9a82 🚀 Use docker pull instead of building repo 2020-08-09 15:24:50 -06:00
af91883635 📝 Overhaul docs
Add better detail of bot creation

Add badges

Add sample docker-compose.yml
2020-08-09 15:24:15 -06:00
18 changed files with 475 additions and 141 deletions

View File

@ -1,8 +1,10 @@
FROM python:3.7-slim FROM python:3.7-slim
LABEL maintainer="Joshua Arulsamy <joshua.gf.arul@gmail.com>"
# Install ffmpeg # Install ffmpeg
RUN apt-get -y update && \ RUN apt-get -y update && \
apt-get install -y --no-install-recommends ffmpeg && \ apt-get install -y --no-install-recommends ffmpeg=7:4.1.6-1~deb10u1 && \
apt-get autoremove -y && \ apt-get autoremove -y && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@ -14,7 +16,7 @@ WORKDIR /src
COPY requirements.txt . COPY requirements.txt .
# Install all dependencies. # Install all dependencies.
RUN pip install --only-binary all --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy PlexBot over to src. # Copy PlexBot over to src.
COPY PlexBot/ PlexBot COPY PlexBot/ PlexBot

18
Jenkinsfile vendored
View File

@ -25,6 +25,7 @@ pipeline {
sh ''' conda create --yes -n ${BUILD_TAG} python sh ''' conda create --yes -n ${BUILD_TAG} python
source /var/lib/jenkins/miniconda3/etc/profile.d/conda.sh source /var/lib/jenkins/miniconda3/etc/profile.d/conda.sh
conda activate ${BUILD_TAG} conda activate ${BUILD_TAG}
pip install -r requirements.txt
pip install pylint pip install pylint
''' '''
} }
@ -47,19 +48,12 @@ pipeline {
} }
} }
steps { steps {
sh ''' source /var/lib/jenkins/miniconda3/etc/profile.d/conda.sh sh './deploy/build.sh'
conda activate ${BUILD_TAG}
python deploy/build.py
'''
} }
post { }
always { stage('Push Image') {
// Archive unit tests for the future steps {
archiveArtifacts (allowEmptyArchive: true, sh './deploy/push.sh'
artifacts: 'dist/*whl',
fingerprint: true)
sh 'python deploy/push.py'
}
} }
} }
} }

17
Makefile Normal file
View File

@ -0,0 +1,17 @@
.PHONY: help pull build clean
.DEFAULT_GOAL: build
help:
@echo "make pull"
@echo " Start docker container with pull"
@echo "make build"
@echo " Start docker container rebuilding container"
pull:
docker-compose up
build:
docker-compose -f docker-compose_dev.yml up --build
clean:
docker system prune -a

View File

@ -1,8 +1,8 @@
"""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.
Do not import this module, it is intended to be
used exclusively within a docker environment.
""" """
import logging import logging
import sys import sys
@ -56,4 +56,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":
config["lyrics"]["token"] = None
return config return config

View File

@ -1,7 +1,6 @@
"""Main entrypoint for bot """
Main entrypoint script.
Sets up loggers and initiates bot. Sets up loggers and initiates bot.
""" """
import logging import logging
@ -21,6 +20,8 @@ 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"]
# Set appropiate log level # Set appropiate log level
root_log = logging.getLogger() root_log = logging.getLogger()
plex_log = logging.getLogger("Plex") plex_log = logging.getLogger("Plex")
@ -29,7 +30,16 @@ bot_log = logging.getLogger("Bot")
plex_log.setLevel(config["plex"]["log_level"]) plex_log.setLevel(config["plex"]["log_level"])
bot_log.setLevel(config["discord"]["log_level"]) bot_log.setLevel(config["discord"]["log_level"])
plex_args = {
"base_url": BASE_URL,
"plex_token": PLEX_TOKEN,
"lib_name": LIBRARY_NAME,
"lyrics_token": LYRICS_TOKEN,
}
bot = Bot(command_prefix=BOT_PREFIX) bot = Bot(command_prefix=BOT_PREFIX)
# Remove help command, we have our own custom one.
bot.remove_command("help")
bot.add_cog(General(bot)) bot.add_cog(General(bot))
bot.add_cog(Plex(bot, BASE_URL, PLEX_TOKEN, LIBRARY_NAME, BOT_PREFIX)) bot.add_cog(Plex(bot, **plex_args))
bot.run(TOKEN) bot.run(TOKEN)

View File

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

View File

@ -1,22 +1,44 @@
""" All discord bot and Plex api interactions""" """All discord bot and Plex api interactions."""
import asyncio import asyncio
import io import io
import logging import logging
from urllib.request import urlopen from urllib.request import urlopen
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 fuzzywuzzy import fuzz
from plexapi.exceptions import Unauthorized from plexapi.exceptions import Unauthorized
from plexapi.server import PlexServer from plexapi.server import PlexServer
from .exceptions import MediaNotFoundError
from .exceptions import VoiceChannelError
root_log = logging.getLogger() root_log = logging.getLogger()
plex_log = logging.getLogger("Plex") plex_log = logging.getLogger("Plex")
bot_log = logging.getLogger("Bot") bot_log = logging.getLogger("Bot")
help_text = """
General:
kill [silent] - Halt the bot [silently].
help - Print this help message.
cleanup - Delete old messages from the bot.
Plex:
play <SONG_NAME> - Play a song from the plex server.
album <ALBUM_NAME> - Queue an entire album to play.
lyrics - Print the lyrics of the song (Requires Genius API)
np - Print the current playing song.
stop - Halt playback and leave vc.
pause - Pause playback.
resume - Resume playback.
clear - Clear play queue.
[] - Optional args.
"""
class General(commands.Cog): class General(commands.Cog):
"""General commands """General commands
@ -58,6 +80,22 @@ class General(commands.Cog):
await self.bot.close() await self.bot.close()
bot_log.info("Stopping upon the request of %s", ctx.author.mention) bot_log.info("Stopping upon the request of %s", ctx.author.mention)
@command(name="help")
async def help(self, ctx):
"""Prints command help
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raise:
None
"""
await ctx.send(f"```{help_text}```")
@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
@ -83,26 +121,33 @@ class General(commands.Cog):
except (discord.Forbidden, discord.NotFound, discord.HTTPException): except (discord.Forbidden, discord.NotFound, discord.HTTPException):
pass pass
async for i in channel.history(limit=limit):
if i.author == ctx.message.author and i.content.startswith(
self.bot.command_prefix
):
try:
await i.delete()
except (discord.Forbidden, discord.NotFound, discord.HTTPException):
pass
except discord.Forbidden: except discord.Forbidden:
bot_log.info("Unable to delete messages, insufficient permissions.") bot_log.info("Unable to delete messages, insufficient permissions.")
await ctx.send("I don't have the necessary permissions to delete messages.") await ctx.send("I don't have the necessary permissions to delete messages.")
class Plex(commands.Cog): class Plex(commands.Cog):
"""Discord commands pertinent to interacting with Plex """
Discord commands pertinent to interacting with Plex
Contains user commands such as play, pause, resume, stop, etc.
Grabs, and parses all data from plex database.
Contains user commands such as play, pause, resume, stop, etc.
Grabs, and parses all data from plex database.
""" """
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
# All are necessary to detect global interactions # All are necessary to detect global interactions
# within the bot. # within the bot.
def __init__( def __init__(self, bot, **kwargs):
self, bot, base_url: str, plex_token: str, lib_name: str, bot_prefix: str
):
"""Initializes Plex resources """Initializes Plex resources
Connects to Plex library and sets up Connects to Plex library and sets up
@ -113,7 +158,6 @@ class Plex(commands.Cog):
base_url: str url to Plex server base_url: str url to Plex server
plex_token: str X-Token of Plex server plex_token: str X-Token of Plex server
lib_name: str name of Plex library to search through lib_name: str name of Plex library to search through
bot_prefix: str prefix used to interact with bots
Raises: Raises:
plexapi.exceptions.Unauthorized: Invalid Plex token plexapi.exceptions.Unauthorized: Invalid Plex token
@ -123,10 +167,16 @@ class Plex(commands.Cog):
""" """
self.bot = bot self.bot = bot
self.base_url = base_url self.base_url = kwargs["base_url"]
self.plex_token = plex_token self.plex_token = kwargs["plex_token"]
self.library_name = lib_name self.library_name = kwargs["lib_name"]
self.bot_prefix = bot_prefix self.bot_prefix = bot.command_prefix
if kwargs["lyrics_token"]:
self.genius = lyricsgenius.Genius(kwargs["lyrics_token"])
else:
plex_log.warning("No lyrics token specified, lyrics disabled")
self.genius = None
# Log fatal invalid plex token # Log fatal invalid plex token
try: try:
@ -152,32 +202,40 @@ 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 """Search the Plex music db for track
Uses a fuzzy search algorithm to find the closest matching song
title in the Plex music database.
Args: Args:
title: str title of song to search for title: str title of song to search for
Returns: Returns:
plexapi.audio.Track pointing to closest matching title plexapi.audio.Track pointing to best matching title
None if song can't be found.
Raises: Raises:
None MediaNotFoundError: Title of track can't be found in plex db
""" """
tracks = self.music.searchTracks() results = self.music.searchTracks(title=title, maxresults=1)
score = [None, -1] try:
for i in tracks: return results[0]
ratio = fuzz.ratio(title.lower(), i.title.lower()) except IndexError:
if ratio > score[1]: raise MediaNotFoundError("Track cannot be found")
score[0] = i
score[1] = ratio
elif ratio == score[1]:
score[0] = i
return score[0] def _search_albums(self, title: str):
"""Search the Plex music db for album
Args:
title: str title of album to search for
Returns:
plexapi.audio.Album pointing to best matching title
Raises:
MediaNotFoundError: Title of album can't be found in plex db
"""
results = self.music.searchAlbums(title=title, maxresults=1)
try:
return results[0]
except IndexError:
raise MediaNotFoundError("Album cannot be found")
async def _play(self): async def _play(self):
"""Heavy lifting of playing songs """Heavy lifting of playing songs
@ -204,7 +262,7 @@ class Plex(commands.Cog):
plex_log.debug("%s - URL: %s", self.current_track, track_url) plex_log.debug("%s - URL: %s", self.current_track, track_url)
embed, img = self._build_embed(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 _audio_player_task(self): async def _audio_player_task(self):
@ -259,11 +317,12 @@ class Plex(commands.Cog):
self.current_track = None self.current_track = None
self.bot.loop.call_soon_threadsafe(self.play_next_event.set) self.bot.loop.call_soon_threadsafe(self.play_next_event.set)
def _build_embed(self, track, type_="play"): @staticmethod
"""Creates a pretty embed card. def _build_embed_track(track, type_="play"):
"""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, artistm and album art. All Status, song title, album, artist and album art. All
pertitent information is grabbed dynamically from the Plex db. pertitent information is grabbed dynamically from the Plex db.
Args: Args:
@ -271,7 +330,7 @@ class Plex(commands.Cog):
type_: Type of card to make (play, queue). type_: Type of card to make (play, queue).
Returns: Returns:
embed: discord.embed fully constructed img payload. embed: discord.embed fully constructed payload.
thumb_art: io.BytesIO of album thumbnail img. thumb_art: io.BytesIO of album thumbnail img.
Raises: Raises:
@ -302,13 +361,72 @@ class Plex(commands.Cog):
# Point to file attached with ctx object. # Point to file attached with ctx object.
embed.set_thumbnail(url="attachment://image0.png") embed.set_thumbnail(url="attachment://image0.png")
bot_log.debug("Built embed for %s", track.title) bot_log.debug("Built embed for track - %s", track.title)
return embed, art_file return embed, art_file
@staticmethod
def _build_embed_album(album):
"""Creates a pretty embed card for albums
Builds a helpful status embed with the following info:
album, artist, and album art. All pertitent information
is grabbed dynamically from the Plex db.
Args:
album: plexapi.audio.Album object of album
Returns:
embed: discord.embed fully constructed payload.
thumb_art: io.BytesIO of album thumbnail img.
Raises:
None
"""
# Grab the relevant thumbnail
img_stream = urlopen(album.thumbUrl)
img = io.BytesIO(img_stream.read())
# Attach to discord embed
art_file = discord.File(img, filename="image0.png")
title = "Added album to queue"
descrip = f"{album.title} - {album.artist().title}"
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 album - %s", album.title)
return embed, art_file
async def _validate(self, ctx):
"""Ensures user is in a vc
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
VoiceChannelError: Author not in voice channel
"""
# Fail if user not in vc
if not ctx.author.voice:
await ctx.send("Join a voice channel first!")
bot_log.debug("Failed to play, requester not in voice channel")
raise VoiceChannelError
# Connect to voice if not already
if not self.voice_channel:
self.voice_channel = await ctx.author.voice.channel.connect()
bot_log.debug("Connected to vc.")
@command() @command()
async def play(self, ctx, *args): async def play(self, ctx, *args):
"""User command to start playback """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.
@ -325,41 +443,69 @@ class Plex(commands.Cog):
""" """
# Save the context to use with async callbacks # Save the context to use with async callbacks
self.ctx = ctx self.ctx = ctx
if not args:
await ctx.send(f"Usage: {self.bot_prefix}play TITLE_OF_SONG")
bot_log.debug("Failed to play, invalid usage")
return
title = " ".join(args) title = " ".join(args)
track = self._search_tracks(title)
# Fail if song title can't be found try:
if not track: track = self._search_tracks(title)
except MediaNotFoundError:
await ctx.send(f"Can't find song: {title}") await ctx.send(f"Can't find song: {title}")
bot_log.debug("Failed to play, can't find song - %s", title) bot_log.debug("Failed to play, can't find song - %s", title)
return return
# Fail if user not in vc try:
if not ctx.author.voice: await self._validate(ctx)
await ctx.send("Join a voice channel first!") except VoiceChannelError:
bot_log.debug("Failed to play, requester not in voice channel") pass
return
# Connect to voice if not already
if not self.voice_channel:
self.voice_channel = await ctx.author.voice.channel.connect()
bot_log.debug("Connected to vc.")
# Specific add to queue message # Specific add to queue message
if self.voice_channel.is_playing(): if 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, 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)
# Add the song to the async queue # Add the song to the async queue
await self.play_queue.put(track) await self.play_queue.put(track)
@command()
async def album(self, ctx, *args):
"""User command to play song
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 song to play
Returns:
None
Raises:
None
"""
# Save the context to use with async callbacks
self.ctx = ctx
title = " ".join(args)
try:
album = self._search_albums(title)
except MediaNotFoundError:
await ctx.send(f"Can't find album: {title}")
bot_log.debug("Failed to queue album, can't find - %s", title)
return
try:
await self._validate(ctx)
except VoiceChannelError:
pass
bot_log.debug("Added to queue - %s", title)
embed, img = self._build_embed_album(album)
await ctx.send(embed=embed, file=img)
for track in album.tracks():
await self.play_queue.put(track)
@command() @command()
async def stop(self, ctx): async def stop(self, ctx):
"""User command to stop playback """User command to stop playback
@ -461,7 +607,7 @@ class Plex(commands.Cog):
None None
""" """
if self.current_track: if self.current_track:
embed, img = self._build_embed(self.current_track) embed, img = self._build_embed_track(self.current_track)
bot_log.debug("Now playing") bot_log.debug("Now playing")
if self.np_message_id: if self.np_message_id:
await self.np_message_id.delete() await self.np_message_id.delete()
@ -486,3 +632,54 @@ class Plex(commands.Cog):
self.play_queue = asyncio.Queue() self.play_queue = asyncio.Queue()
bot_log.debug("Cleared queue") bot_log.debug("Cleared queue")
await ctx.send(":boom: Queue cleared.") await ctx.send(":boom: Queue cleared.")
@command()
async def lyrics(self, ctx):
"""User command to get lyrics of a song.
Args:
ctx: discord.ext.commands.Context message context from command
Returns:
None
Raises:
None
"""
if not self.current_track:
plex_log.info("No song currently playing")
return
if self.genius:
plex_log.info(
"Searching for %s, %s",
self.current_track.title,
self.current_track.artist().title,
)
try:
song = self.genius.search_song(
self.current_track.title, self.current_track.artist().title
)
except TypeError:
self.genius = None
plex_log.error("Invalid genius token, disabling lyrics")
return
try:
lyrics = song.lyrics
# Split into 1950 char chunks
# Discord max message length is 2000
lines = [(lyrics[i : i + 1950]) for i in range(0, len(lyrics), 1950)]
for i in lines:
if i == "":
continue
# Apply code block format
i = f"```{i}```"
await ctx.send(i)
except (IndexError, TypeError):
plex_log.info("Could not find lyrics")
await ctx.send("Can't find lyrics for this song.")
else:
plex_log.warning("Attempted lyrics without valid token")

10
PlexBot/exceptions.py Normal file
View File

@ -0,0 +1,10 @@
class MediaNotFoundError(Exception):
"""Raised when a PlexAPI media resource cannot be found."""
pass
class VoiceChannelError(Exception):
"""Raised when user is not connected to a voice channel."""
pass

145
README.md
View File

@ -1,28 +1,71 @@
# Plex-Bot # Plex-Bot
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/c93b8ff976ce4205a95046487917476b)](https://app.codacy.com/manual/jarulsamy/Plex-Bot?utm_source=github.com&utm_medium=referral&utm_content=jarulsamy/Plex-Bot&utm_campaign=Badge_Grade_Dashboard)
[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html)
![docker pulls](https://img.shields.io/docker/pulls/jarulsamy/plex-bot)
![docker img size](https://img.shields.io/docker/image-size/jarulsamy/plex-bot)
![black badge](https://img.shields.io/badge/code%20style-black-000000.svg)
A Python-based Plex music bot for discord. A Python-based Plex music bot for discord.
![screenshot](assets/screenshot.png)
## Setup ## Setup
Plex-Bot runs entirely in a Docker container. Ensure you have Docker and docker-compose installed according to the official Docker [documentation](https://docs.docker.com/get-docker/). Plex-Bot runs entirely in a Docker container. Ensure you have Docker and docker-compose installed according to the official Docker [documentation](https://docs.docker.com/get-docker/).
1. Clone the repository and `cd` into it: 1. Create a new folder and `cd` into it:
``` ```bash
$ git clone https://github.com/jarulsamy/Plex-Bot mkdir Plex-Bot
$ cd Plex-Bot cd Plex-Bot
``` ```
2. Create a configuration folder: 2. Make a `docker-compose.yml` file or use this sample:
Create a new `config` folder and copy the sample config file into it: ```yml
version: "3"
services:
plex-bot:
container_name: "PlexBot"
image: jarulsamy/plex-bot:latest
environment:
- PUID=1000
- PGID=1000
- TZ=America/Denver
# Required dir for configuration files
volumes:
- "./config:/config:ro"
restart: "unless-stopped"
```
``` 3. Create a new `config` folder and create a config file like this::
$ mkdir config
$ cp sample-config.yaml config/config.yaml
```
3. Create a Discord bot application: ```bash
mkdir config
cd config
touch config.yaml
```
```yml
# Create a file called config.yaml with the following contents
root:
log_level: "info"
discord:
prefix: "?"
token: "<BOT_TOKEN>"
log_level: "debug"
plex:
base_url: "<BASE_URL>"
token: "<PLEX_TOKEN>"
library_name: "<LIBRARY_NAME>"
log_level: "debug"
```
4. Create a Discord bot application:
1. Go to the Discord developer portal, [here](https://discord.com/developers/applications). 1. Go to the Discord developer portal, [here](https://discord.com/developers/applications).
@ -38,44 +81,70 @@ $ cp sample-config.yaml config/config.yaml
6. Click Create Bot User 6. Click Create Bot User
This will provide you with your bot Username and Token This will provide you with your bot Username and Token
7. Fill in all the necessary numbers in `config/config.yaml` 7. Fill in the bot token in `config/config.yaml`
4. Get your plex token: 5. Get your plex token:
Refer to the official [plex documentation](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/). * Refer to the official [plex documentation](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/).
Add it to `config/config.yaml` in the appropiate spot. * Add it to `config/config.yaml` in the appropiate spot.
5. Start the service: 6. Get your Lyrics Genius token (Optional):
``` If you wanty to disable this feature, set token to `None` in `config/config.yaml`
$ docker-compose up --build
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:
1. Click New API Client
2. Set the app website url to: `https://github.com/jarulsamy/Plex-Bot`
3. Set the redirect url to: `http://localhost`
4. Copy the **Client Access Token** to `config/config.yaml`
7. Customize remaining settings
Set any remaining settings in the config file that you would like. Such as music library, and base url of the Plex server.
8. Start the service:
```bash
docker-compose up -d
```
## Logs
You can view the logs with the following command
```bash
docker-compose logs -f CONTAINER_NAME_OR_ID
# For example
docker-compose logs -f PlexBot
``` ```
## Usage ## Usage
``` ```text
General: General:
kill - Stop the bot. kill [silent] - Halt the bot [silently].
help - Print this help message.
cleanup - Delete old messages from the bot.
Plex: Plex:
np - View currently playing song. play <SONG_NAME> - Play a song from the plex server.
pause - Pause currently playing song. album <ALBUM_NAME> - Queue an entire album to play.
play - Play a song from the Plex library. lyrics - Print the lyrics of the song (Requires Genius API)
resume - Resume a paused song. np - Print the current playing song.
skip - Skip a song. stop - Halt playback and leave vc.
stop - Stop playing. pause - Pause playback.
No Category: resume - Resume playback.
help Shows this message clear - Clear play queue.
Type ?help command for more info on a command. [] - Optional args.
You can also type ?help category for more info on a category.
``` ```
## Support
Reach out to me at one of the following places!
- Email (Best) at joshua.gf.arul@gmail.com
- Twitter at <a href="http://twitter.com/jarulsamy_" target="_blank">`@jarulsamy_`</a>
* * * * * *

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,8 +0,0 @@
import os
import sys
sys.path.append("PlexBot")
from __version__ import VERSION
sys.exit(os.system(f"docker build -t jarulsamy/plex-bot:{VERSION} ."))

14
deploy/build.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
VERSION=$(python PlexBot/__version__.py)
docker build -t "jarulsamy/plex-bot:$VERSION" .
if [ $? -eq 0 ]
then
echo "Successfully build docker image."
exit 0
else
echo "Failed to build docker image." >&2
exit 1
fi

View File

@ -1,8 +0,0 @@
import os
import sys
sys.path.append("PlexBot")
from __version__ import VERSION
sys.exit(os.system(f"docker push jarulsamy/plex-bot:{VERSION}"))

14
deploy/push.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
VERSION=$(python PlexBot/__version__.py)
docker push "jarulsamy/plex-bot:$VERSION"
if [ $? -eq 0 ]
then
echo "Successfully pushed docker image."
exit 0
else
echo "Failed to push docker image." >&2
exit 1
fi

View File

@ -2,7 +2,7 @@ version: "3"
services: services:
plex-bot: plex-bot:
container_name: "PlexBot" container_name: "PlexBot"
build: . image: jarulsamy/plex-bot:latest
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
@ -10,4 +10,4 @@ services:
# Required dir for configuration files # Required dir for configuration files
volumes: volumes:
- "./config:/config:ro" - "./config:/config:ro"
restart: "no" restart: "unless-stopped"

13
docker-compose_dev.yml Normal file
View File

@ -0,0 +1,13 @@
version: "3"
services:
plex-bot:
container_name: "PlexBot"
build: .
environment:
- PUID=1000
- PGID=1000
- TZ=America/Denver
# Required dir for configuration files
volumes:
- "./config:/config:ro"
restart: "no"

View File

@ -1,6 +1,7 @@
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
ffmpeg==1.4 ffmpeg==1.4
PyYAML==5.3.1 PyYAML==5.3.1
lyricsgenius==2.0.0

View File

@ -11,3 +11,6 @@ plex:
token: "<PLEX_TOKEN>" token: "<PLEX_TOKEN>"
library_name: "<LIBRARY_NAME>" library_name: "<LIBRARY_NAME>"
log_level: "debug" log_level: "debug"
lyrics:
token: <CLIENT_ACCESS_TOKEN>