Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support for plex #8

Merged
merged 1 commit into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added api/plex/__init__.py
Empty file.
195 changes: 195 additions & 0 deletions api/plex/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import json
import discord
from discord.utils import utcnow

from config.globals import PLEX_ICON, DISCORD_THUMBNAIL, PLEX_PLAYING, PLEX_CONTENT

from utils.custom_logger import logger

class PlexWebhookHandler:
def __init__(self, payload, discord_bot):
self.payload = payload
self.discord_bot = discord_bot

# Global variables for the class to use in the embed creation
self.media_type = self.payload.get('source_metadata_details', {}).get('media_type', 'N/A').capitalize()
self.year = self.payload.get('source_metadata_details', {}).get('year', 'N/A')
self.title = self.payload.get('source_metadata_details', {}).get('title', 'N/A')
self.summary = self.payload.get('source_metadata_details', {}).get('summary', 'N/A')
self.quality = self.payload.get('source_metadata_details', {}).get('video_full_resolution', 'N/A')
self.air_date = self.payload.get('source_metadata_details', {}).get('air_date', 'N/A')
self.genres = self.payload.get('source_metadata_details', {}).get('genres', 'N/A')
self.release_date = self.payload.get('source_metadata_details', {}).get('release_date', 'N/A')
self.season_number = self.payload.get('source_metadata_details', {}).get('season_num00', 'N/A')
self.episode_number = self.payload.get('source_metadata_details', {}).get('episode_num00', 'N/A')
self.episode_count = self.payload.get('source_metadata_details', {}).get('episode_count', 'N/A')
self.poster_url = self.payload.get('source_metadata_details', {}).get('poster_url', 'N/A')
self.imdb_url = self.payload.get('source_metadata_details', {}).get('imdb_url', 'N/A')
self.tvdb_url = self.payload.get('source_metadata_details', {}).get('thetvdb_url', 'N/A')
self.rating = self.payload.get('source_metadata_details', {}).get('rating', 'N/A')
self.username = self.payload.get('stream_details', {}).get('username', 'N/A')
self.product = self.payload.get('stream_details', {}).get('product', 'N/A')
self.video_decision = self.payload.get('stream_details', {}).get('video_decision', 'N/A').capitalize()
self.remaining_time = self.payload.get('stream_details', {}).get('remaining_time', 'N/A')
self.duration_time = self.payload.get('stream_details', {}).get('duration_time', 'N/A')
self.server_name = self.payload.get('server_info', {}).get('server_name', 'N/A')
self.webhook_type = self.payload.get('server_info', {}).get('webhook_type', 'N/A')

async def handle_webhook(self):
logger.info(f"Processing Plex webhook payload for event type: {self.webhook_type}")
logger.debug(f"Payload: {json.dumps(self.payload, indent=4)}")
channel_id = self.determine_channel_id()
await self.dispatch_embed(channel_id)

def determine_channel_id(self):
channel_ids = {
'nowplaying': PLEX_PLAYING,
'nowresuming': PLEX_PLAYING,
'finished': PLEX_PLAYING,
'newcontent_episode': PLEX_CONTENT,
'newcontent_season': PLEX_CONTENT,
'newcontent_movie': PLEX_CONTENT,
}
return channel_ids.get(self.webhook_type, 'default_channel_id')

def generate_embed(self):
# Use a dictionary to map event types to their respective embed creation methods
embed_creators = {
'nowplaying': self.embed_for_nowplaying,
'nowresuming': self.embed_for_nowresuming,
'finished': self.embed_for_finished,
'newcontent_episode': self.embed_for_newcontent_episode,
'newcontent_season': self.embed_for_newcontent_season,
'newcontent_movie': self.embed_for_newcontent_movie,
}

# Get the embed creation method for the event type
generate_embed = embed_creators.get(self.webhook_type, self.embed_for_default)

# Call the embed creation method and return the embed
return generate_embed()

def init_embed(self, title, color, author):
embed = discord.Embed(title=title, color=color)
embed.set_author(name=author, icon_url=PLEX_ICON)
embed.url = self.imdb_url if self.imdb_url != 'N/A' else self.tvdb_url
timestamp = utcnow()
embed.timestamp = timestamp
embed.set_image(url=DISCORD_THUMBNAIL)

return embed

def embed_for_nowplaying(self):
embed_title = f"{self.title} ({self.year})" if self.media_type == "Movie" else f"{self.title} (S{self.season_number}E{self.episode_number})"
embed = self.init_embed(embed_title, 0x1C4673, f"Plex - Streaming {self.media_type}")

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

embed.add_field(name=":arrow_forward: Now Streaming", value=f"{self.remaining_time} remaining", inline=True)
embed.set_footer(text=f"{self.server_name} | {self.username} | {self.product} | {self.video_decision}")

return embed

def embed_for_nowresuming(self):
embed_title = f"{self.title} ({self.year})" if self.media_type == "Movie" else f"{self.title} (S{self.season_number}E{self.episode_number})"
embed = self.init_embed(embed_title, 0x3587DE, f"Plex - Streaming {self.media_type}")

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

embed.add_field(name=":play_pause: Now Resuming", value=f"{self.remaining_time} remaining", inline=True)
embed.set_footer(text=f"{self.server_name} | {self.username} | {self.product} | {self.video_decision}")

return embed

def embed_for_finished(self):
embed_title = f"{self.title} ({self.year})" if self.media_type == "Movie" else f"{self.title} (S{self.season_number}E{self.episode_number})"
embed = self.init_embed(embed_title, 0x891836, f"Plex - Streaming {self.media_type}")

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

embed.add_field(name=":white_check_mark: Finished", value=f"{self.duration_time} total", inline=True)
embed.set_footer(text=f"{self.server_name} | {self.username} | {self.product} | {self.video_decision}")

return embed

def embed_for_newcontent_episode(self):
embed_title = f"{self.title} (S{self.season_number}E{self.episode_number})"
embed = self.init_embed(embed_title, 0xE91655, "Plex - New Episode")
embed.description = self.summary

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

fields = {
"Quality": (self.quality, True),
"Runtime": (self.remaining_time, True),
"Air date": (self.air_date, True),
}

for name, (value, inline) in fields.items():
embed.add_field(name=name, value=value, inline=inline)

embed.set_footer(text=self.server_name)

return embed

def embed_for_newcontent_season(self):
embed_title = self.title
embed = self.init_embed(embed_title, 0x169CE9, "Plex - New Season")
embed.description = self.summary

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

fields = {
"Season": (self.season_number, True),
"Episodes": (self.episode_count, True),
"Details": (f"[IMDb]({self.imdb_url})", True),
}

for name, (value, inline) in fields.items():
embed.add_field(name=name, value=value, inline=inline)

embed.set_footer(text=self.server_name)

return embed

def embed_for_newcontent_movie(self):
embed_title = f"{self.title} ({self.year})"
embed = self.init_embed(embed_title, 0x7CE916, "Plex - New Movie")
embed.description = self.summary

poster_path = self.poster_url
if poster_path:
embed.set_thumbnail(url=poster_path)

fields = {
"Quality": (self.quality, True),
"Genres": (self.genres, True),
"Release date": (self.release_date, True),
"Rotten Tomatoes": (f":popcorn: {self.rating}", True),
"Runtime": (self.remaining_time, True),
}

for name, (value, inline) in fields.items():
embed.add_field(name=name, value=value, inline=inline)

embed.set_footer(text=self.server_name)

return embed

def embed_for_default(self):
embed = discord.Embed (title=" ")
return embed

async def dispatch_embed(self, channel_id):
embed = self.generate_embed()
await self.discord_bot.dispatch_embed(channel_id, embed)
5 changes: 4 additions & 1 deletion api/radarr/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
from utils.custom_logger import logger

from api.tmdb.client import TMDb
from src.radarr.functions import process_webhook
from config.globals import RADARR_CHANNEL

class RadarrWebhookHandler:
def __init__(self, payload, discord_bot):
Expand All @@ -26,9 +28,10 @@ def initialize_global_variables(self):
self.custom_formats = self.release_data.get('customFormats', [])
self.tmdb_id = self.movie.get('tmdbId', 'N/A')
self.embed_title = f"{self.movie_title} ({self.movie_year})"
self.poster = TMDb.movie_poster_path(self.tmdb_id)

async def handle_webhook(self):
logger.info(f"Processing Radarr webhook payload for event type: {self.event_type}")
logger.debug(f"Payload: {json.dumps(self.payload, indent=4)}")
channel = self.discord_bot.bot.get_channel(1052967176828616724)
channel = self.discord_bot.bot.get_channel(RADARR_CHANNEL)
await process_webhook(self, channel)
5 changes: 4 additions & 1 deletion api/sonarr/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
from utils.custom_logger import logger

from api.tmdb.client import TMDb
from src.sonarr.functions import process_webhook
from config.globals import SONARR_CHANNEL

class SonarrWebhookHandler:
def __init__(self, payload, discord_bot):
Expand All @@ -27,6 +29,7 @@ def initialize_global_variables(self):
self.custom_format_score = self.release_data.get('customFormatScore', 'N/A')
self.custom_formats = self.release_data.get('customFormats', [])
self.episode_count = len(self.episodes)
self.poster = TMDb.show_poster_path(self.tvdb_id)

def check_request_type(self):
if self.episode_count > 1:
Expand All @@ -50,5 +53,5 @@ def handle_episode_request(self):
async def handle_webhook(self):
logger.info(f"Processing Sonarr webhook payload for event type: {self.event_type}")
logger.debug(f"Payload: {json.dumps(self.payload, indent=4)}")
channel = self.discord_bot.bot.get_channel(1052967176828616724)
channel = self.discord_bot.bot.get_channel(SONARR_CHANNEL)
await process_webhook(self, channel)
17 changes: 16 additions & 1 deletion config/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@
# Discord
DISCORD_SERVER_ID = os.getenv("DISCORD_SERVER_ID")
DISCORD_TOKEN = os.environ["DISCORD_TOKEN"]
DISCORD_THUMBNAIL = "https://i.postimg.cc/KvSTwcQ0/undefined-Imgur.png"

# Plex
PLEX_PLAYING = int(os.getenv("PLEX_PLAYING"))
PLEX_CONTENT = int(os.getenv("PLEX_CONTENT"))
PLEX_ICON = "https://i.imgur.com/ZuFghbX.png"

# Trakt
TRAKT_API_URL = "https://api.trakt.tv"
TRAKT_ICON = "https://i.imgur.com/tvnkxAY.png"
TRAKT_CHANNEL = int(os.getenv("TRAKT_CHANNEL"))
TRAKT_CLIENT_ID = os.getenv("TRAKT_CLIENTID")
TRAKT_USERNAME = os.getenv("TRAKT_USERNAME")

# TMDB
TMDB_API_KEY = os.getenv("TMDB_API_KEY")
TMDB_API_KEY = os.getenv("TMDB_API_KEY")

# Arrs
RADARR_CHANNEL = int(os.getenv("RADARR_CHANNEL"))
RADARR_ICON = "https://i.imgur.com/6U4aXO0.png"
SONARR_CHANNEL = int(os.getenv("SONARR_CHANNEL"))
SONARR_ICON = "https://i.imgur.com/dZSIKZE.png"
61 changes: 13 additions & 48 deletions src/discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
from discord.ext import commands

from utils.custom_logger import logger
from utils.datetime import get_times
from src.discord.embed import EmbedBuilder
from api.trakt.client import TraktClient

class DiscordBot:
def __init__(self, token):
Expand Down Expand Up @@ -41,48 +38,16 @@ async def on_ready(self):
logger.info('Loading tasks cog')
await self.bot.load_extension('src.discord.cogs.tasks')

# Load commands
@self.bot.command()
async def favorite(ctx, username: str = None):
if username is None:
await ctx.send('No username given!')
return

logger.info(f'Trakt command invoked with username: {username}')
client = TraktClient()
user = client.user(username)
# Get the current time and the time one hour ago
now, one_hour_ago = get_times()
favorites = user.get_favorites(start_time=one_hour_ago, end_time=now)
# Add the last favorite to the embed
if favorites:
latest_favorite = favorites[-1]
# Create an EmbedBuilder instance
embed_builder = EmbedBuilder(description=f'{username} has favorited {latest_favorite.title}')
else:
embed_builder = EmbedBuilder(description=f'{username} has no favorites')
# Send the embed to the channel where the command was invoked
await embed_builder.send_embed(ctx.channel)

@self.bot.command()
async def rating(ctx, username: str = None):
if username is None:
await ctx.send('No username given!')
return

logger.info(f'Trakt command invoked with username: {username}')
client = TraktClient()
user = client.user(username)
# Get the current time and the time one hour ago
now, one_hour_ago = get_times()
ratings = user.get_ratings(start_time=one_hour_ago, end_time=now)
# Add the last favorite to the embed
if ratings:
latest_rating = ratings[-1]
# Create an EmbedBuilder instance
embed_builder = EmbedBuilder(description=f'{username} has rated "**{latest_rating.title}**"\n{latest_rating.rated_at} with a score of {latest_rating.rated}')
embed_builder.set_footer(text=f"Media type: {latest_rating.type}")
else:
embed_builder = EmbedBuilder(description=f'{username} has no ratings')
# Send the embed to the channel where the command was invoked
await embed_builder.send_embed(ctx.channel)
# Load commands here

## Post an single embed to a channel
async def dispatch_embed(self, channel_id, embed):
channel = self.bot.get_channel(channel_id)
if not channel:
logger.error(f"Channel {channel_id} not found")
return
try:
await channel.send(embed=embed)
logger.info(f"Embed dispatched with title: {embed.title} and author: {embed.author.name}")
except Exception as e:
logger.error(f"Error dispatching embed: {e}")
9 changes: 5 additions & 4 deletions src/discord/cogs/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from utils.custom_logger import logger
from src.trakt.functions import process_ratings, process_favorites
from config.globals import TRAKT_CHANNEL, TRAKT_USERNAME

class TasksCog(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
Expand All @@ -12,18 +13,18 @@ def __init__(self, bot: commands.Bot) -> None:
# Task to process recent Trakt ratings
@tasks.loop(minutes=60)
async def trakt_ratings(self):
ratings_channel = self.bot.get_channel(1052967176828616724)
ratings_channel = self.bot.get_channel(TRAKT_CHANNEL)
try:
await process_ratings(ratings_channel, 'desiler')
await process_ratings(ratings_channel, TRAKT_USERNAME)
except Exception as e:
logger.error(f'Error processing recent Trakt ratings: {e}')

# Task to process recent Trakt favorites
@tasks.loop(hours=24)
async def trakt_favorites(self):
favorites_channel = self.bot.get_channel(1052967176828616724)
favorites_channel = self.bot.get_channel(TRAKT_CHANNEL)
try:
await process_favorites(favorites_channel, 'desiler')
await process_favorites(favorites_channel, TRAKT_USERNAME)
except Exception as e:
logger.error(f'Error processing recent Trakt ratings: {e}')

Expand Down
Loading
Loading