diff --git a/.gitignore b/.gitignore index 75162b2..872eb7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Project specific config.yaml +config/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1501b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Python 3.7 +FROM python:3.7 + +# Update system +RUN apt-get -y update +RUN apt-get -y upgrade +# Install ffmpeg +RUN apt-get install -y ffmpeg + +# All source code +WORKDIR /src + +# Copy of dependency manifest +COPY requirements.txt . + +# Install all dependencies. +RUN pip install -r requirements.txt + +# Copy PlexBot over to src. +COPY PlexBot/ PlexBot + +# Run the bot +# CMD ["python", "-OO", "-m", "PlexBot"] +CMD ["python", "-m", "PlexBot"] diff --git a/PlexBot/__init__.py b/PlexBot/__init__.py index b809c0b..e3b062f 100644 --- a/PlexBot/__init__.py +++ b/PlexBot/__init__.py @@ -1,8 +1,37 @@ +import logging +import sys +from pathlib import Path +from typing import Dict + import yaml +FORMAT = "%(asctime)s %(levelname)s: [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s" -def load_config(filename: str) -> None: - with open(filename, "r") as f: - config = yaml.safe_load(f) +logging.basicConfig(format=FORMAT) +logger = logging.getLogger("PlexBot") + + +def load_config(filename: str) -> Dict[str, str]: + + # All config files should be in /config + # for docker deployment. + filename = Path("/config", filename) + try: + with open(filename, "r") as f: + config = yaml.safe_load(f) + except FileNotFoundError: + logging.fatal("Configuration file not found.") + sys.exit(-1) + + # Convert str level type to logging constant + levels = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + level = config["general"]["log_level"] + config["general"]["log_level"] = levels[level.upper()] return config diff --git a/PlexBot/__main__.py b/PlexBot/__main__.py index 5e365a4..8901ad7 100644 --- a/PlexBot/__main__.py +++ b/PlexBot/__main__.py @@ -3,7 +3,11 @@ from discord.ext.commands import Bot from .bot import General from .bot import Plex from PlexBot import load_config +from . import FORMAT +import logging + +# Load config from file config = load_config("config.yaml") BOT_PREFIX = config["discord"]["prefix"] @@ -12,6 +16,12 @@ TOKEN = config["discord"]["token"] BASE_URL = config["plex"]["base_url"] PLEX_TOKEN = config["plex"]["token"] LIBRARY_NAME = config["plex"]["library_name"] +LOG_LEVEL = config["general"]["log_level"] + +# Set appropiate log level +logger = logging.getLogger("PlexBot") +logging.basicConfig(format=FORMAT) +logger.setLevel(LOG_LEVEL) bot = Bot(command_prefix=BOT_PREFIX) bot.add_cog(General(bot)) diff --git a/PlexBot/bot.py b/PlexBot/bot.py index 8484647..f2c05c8 100644 --- a/PlexBot/bot.py +++ b/PlexBot/bot.py @@ -1,3 +1,4 @@ +import logging from queue import Queue import discord @@ -5,8 +6,11 @@ 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): @@ -16,6 +20,7 @@ class General(commands.Cog): 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): @@ -25,12 +30,19 @@ class Plex(commands.Cog): self.plex_token = plex_token self.library_name = lib_name - self.pms = PlexServer(self.base_url, self.plex_token) + 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 = Queue() - # self.callback_ctx = None + + logger.info("Started bot successfully") def _search_tracks(self, title): tracks = self.music.searchTracks() @@ -51,13 +63,24 @@ class Plex(commands.Cog): await ctx.send(f"Hello {member}") async def _after_callback(self, error=None): - track = self.play_queue.get() - audio_stream = FFmpegPCMAudio(track.getStreamURL()) - self.vc.play(audio_stream) - await self.callback_ctx.send(f"Playing {track.title}") + logger.debug("After callbacked") + if self.play_queue.empty(): + self.current_track = None + logger.debug("No tracks left in queue, returning") + else: + track = self.play_queue.get() + audio_stream = FFmpegPCMAudio(track.getStreamURL()) + self.current_track = track + logger.debug(f"Started playing next song in queue: {track.title}") + self.vc.play(audio_stream) + await self.callback_ctx.send(f"Playing {track.title}") @command() async def play(self, ctx, *args): + if not len(args): + await ctx.send(f"Usage: {BOT_PREFIX}play TITLE_OF_SONG") + return + title = " ".join(args) track = self._search_tracks(title) if track: @@ -67,17 +90,22 @@ class Plex(commands.Cog): return if not self.vc: self.vc = await ctx.author.voice.channel.connect() + logger.debug("Connected to vc.") if self.vc.is_playing(): self.play_queue.put(track) self.callback_ctx = ctx await ctx.send(f"Added {track.title} to queue.") + logger.debug(f"Added {track.title} to queue.") else: audio_stream = FFmpegPCMAudio(track_url) self.vc.play(audio_stream, after=self._after_callback) + self.current_track = track + logger.debug(f"Playing {track.title}") await ctx.send(f"Playing {track.title}") else: - await ctx.send("Song not found!") + logger.debug(f"{title} was not found.") + await ctx.send(f"{title} was not found.") @command() async def stop(self, ctx): @@ -85,7 +113,6 @@ class Plex(commands.Cog): self.vc.stop() await self.vc.disconnect() self.vc = None - await ctx.send("Stopped") @command() @@ -102,7 +129,11 @@ class Plex(commands.Cog): @command() async def skip(self, ctx): + logger.debug("Skip") if self.vc: - await self.vc.stop() - if not self.play_queue.empty(): - await self._after_callback() + self.vc.stop() + await self._after_callback() + + @command() + async def np(self, ctx): + await ctx.send(f"Currently playing: {self.current_track.title}") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7746aaa --- /dev/null +++ b/docker-compose.yml @@ -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: unless-stopped diff --git a/requirements.txt b/requirements.txt index cc52a74..b231446 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ fuzzywuzzy==0.18.0 python-Levenshtein==0.12.0 pynacl==1.4.0 ffmpeg==1.4 +PyYAML==5.3.1 diff --git a/sample-config.yaml b/sample-config.yaml index b01ad15..22273fd 100644 --- a/sample-config.yaml +++ b/sample-config.yaml @@ -1,3 +1,7 @@ +general: + # Options: debug, info, warning, error, critical + log_level: "info" + discord: prefix: "?" token: ""