mirror of
				https://github.com/jarulsamy/Plex-Bot.git
				synced 2024-08-19 15:01:55 +02:00 
			
		
		
		
	Compare commits
	
		
			41 Commits
		
	
	
		
			v0.0.6
			...
			8f05f5e27f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8f05f5e27f | ||
|  | eeb430c016 | ||
|  | 1ffa5a7229 | ||
|  | f4cd675502 | ||
|  | 921dfc02b8 | ||
|  | 9fb091e9e1 | ||
|  | efdd604d65 | ||
|  | 7d6060ed15 | ||
|  | d6f5174d20 | ||
|  | 151f650bb2 | ||
|  | 5eceab7a22 | ||
|  | 545654f9a7 | ||
|  | 57490cdf17 | ||
|  | a6be6eac37 | ||
|  | 5ba7d751d0 | ||
|  | d0b6c25359 | ||
|  | db188d968b | ||
|  | b05dd0598b | ||
|  | fc4682c210 | ||
|  | 228c2b480b | ||
|  | 08a235d55e | ||
|  | 880c4d50f1 | ||
|  | f12701f4c5 | ||
|  | f95d5c1fd2 | ||
|  | 56fd4aa5ab | ||
|  | 5f90b17b0e | ||
|  | cf2bc24f8a | ||
|  | 1839cc5d03 | ||
|  | e6c4b84538 | ||
|  | c1cba637b8 | ||
|  | 7f49c4d958 | ||
|  | ed1a64cb52 | ||
|  | 7471da85f7 | ||
|  | 4633247004 | ||
|  | 98a36c6cbc | ||
|  | b7ab589f6e | ||
|  | 4aadd886d5 | ||
|  | 3e90c9e9ef | ||
|  | 33bbf21bab | ||
|  | 253f2a9a82 | ||
|  | af91883635 | 
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							| @@ -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 { |  | ||||||
|                     // Archive unit tests for the future |  | ||||||
|                     archiveArtifacts (allowEmptyArchive: true, |  | ||||||
|                                      artifacts: 'dist/*whl', |  | ||||||
|                                      fingerprint: true) |  | ||||||
|                     sh 'python deploy/push.py' |  | ||||||
|         } |         } | ||||||
|  |         stage('Push Image') { | ||||||
|  |             steps { | ||||||
|  |                 sh './deploy/push.sh' | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Makefile
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
							
								
								
									
										464
									
								
								PlexBot/bot.py
									
									
									
									
									
								
							
							
						
						
									
										464
									
								
								PlexBot/bot.py
									
									
									
									
									
								
							| @@ -1,31 +1,58 @@ | |||||||
| """ 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 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 fuzzywuzzy import fuzz |  | ||||||
| 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 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. | ||||||
|  |     playlist <PLAYLIST_NAME> - Queue an entire playlist 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 | ||||||
|  |  | ||||||
|     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 | ||||||
| @@ -40,7 +67,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 | ||||||
| @@ -58,9 +86,27 @@ 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 | ||||||
|  |  | ||||||
|         Args: |         Args: | ||||||
|             ctx: discord.ext.commands.Context message context from command |             ctx: discord.ext.commands.Context message context from command | ||||||
| @@ -83,27 +129,35 @@ 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. |     Contains user commands such as play, pause, resume, stop, etc. | ||||||
|     Grabs, and parses all data from plex database. |     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 | ||||||
|         all asyncronous communications. |         all asyncronous communications. | ||||||
| @@ -113,7 +167,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 +176,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,35 +211,64 @@ 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") | ||||||
|  |  | ||||||
|  |     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") | ||||||
|  |  | ||||||
|     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. | ||||||
| @@ -204,11 +292,12 @@ 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): | ||||||
|         """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. | ||||||
| @@ -242,7 +331,8 @@ class Plex(commands.Cog): | |||||||
|             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. | ||||||
| @@ -259,11 +349,13 @@ 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,14 +363,14 @@ 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: | ||||||
|             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) |         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 | ||||||
| @@ -302,13 +394,112 @@ 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 = requests.get(album.thumbUrl, stream=True).raw | ||||||
|  |         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 | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def _build_embed_playlist(self, playlist): | ||||||
|  |         """ | ||||||
|  |         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 | ||||||
|  |         img_stream = requests.get(self.pms.url(playlist.composite, True), stream=True).raw | ||||||
|  |         img = io.BytesIO(img_stream.read()) | ||||||
|  |  | ||||||
|  |         # Attach to discord embed | ||||||
|  |         art_file = discord.File(img, filename="image0.png") | ||||||
|  |         title = "Added playlist to queue" | ||||||
|  |         descrip = f"{playlist.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 playlist - %s", playlist.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,44 +516,116 @@ 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() | ||||||
|  |     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) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             playlist = self._search_playlists(title) | ||||||
|  |         except MediaNotFoundError: | ||||||
|  |             await 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(ctx) | ||||||
|  |         except VoiceChannelError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         bot_log.debug("Added to queue - %s", title) | ||||||
|  |         embed, img = self._build_embed_playlist(self, playlist) | ||||||
|  |         await ctx.send(embed=embed, file=img) | ||||||
|  |  | ||||||
|  |         for item in playlist.items(): | ||||||
|  |             if (item.TYPE == "track"): | ||||||
|  |                 await self.play_queue.put(item) | ||||||
|  |  | ||||||
|     @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. | ||||||
|  |  | ||||||
| @@ -385,7 +648,8 @@ class Plex(commands.Cog): | |||||||
|  |  | ||||||
|     @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. | ||||||
| @@ -406,7 +670,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 | ||||||
| @@ -424,7 +689,8 @@ class Plex(commands.Cog): | |||||||
|  |  | ||||||
|     @command() |     @command() | ||||||
|     async def skip(self, ctx): |     async def skip(self, ctx): | ||||||
|         """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. | ||||||
| @@ -446,7 +712,8 @@ class Plex(commands.Cog): | |||||||
|  |  | ||||||
|     @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. | ||||||
| @@ -461,7 +728,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() | ||||||
| @@ -472,7 +739,8 @@ class Plex(commands.Cog): | |||||||
|  |  | ||||||
|     @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 | ||||||
| @@ -486,3 +754,55 @@ 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
									
								
							
							
						
						
									
										10
									
								
								PlexBot/exceptions.py
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										146
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,28 +1,71 @@ | |||||||
| # Plex-Bot | # Plex-Bot | ||||||
|  |  | ||||||
|  | [](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) | ||||||
|  | [](http://perso.crans.org/besson/LICENSE.html) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| A Python-based Plex music bot for discord. | A Python-based Plex music bot for discord. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 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,71 @@ $ 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. |     playlist <PLAYLIST_NAME> - Queue an entire playlist to play. | ||||||
|   resume - Resume a paused song. |     lyrics - Print the lyrics of the song (Requires Genius API) | ||||||
|   skip - Skip a song. |     np - Print the current playing song. | ||||||
|   stop - Stop playing. |     stop - Halt playback and leave vc. | ||||||
| No Category: |     pause - Pause playback. | ||||||
|   help   Shows this message |     resume - Resume playback. | ||||||
|  |     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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 17 KiB | 
| @@ -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
									
								
							
							
						
						
									
										14
									
								
								deploy/build.sh
									
									
									
									
									
										Executable 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 | ||||||
| @@ -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
									
								
							
							
						
						
									
										14
									
								
								deploy/push.sh
									
									
									
									
									
										Executable 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 | ||||||
| @@ -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
									
								
							
							
						
						
									
										13
									
								
								docker-compose_dev.yml
									
									
									
									
									
										Normal 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" | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user