feat: Jellyfin integration finished

- Finsihed Jellyfin integration with auto-add and auto-remove
 - Integration toggle in config

task: none
This commit is contained in:
Yoruio
2022-07-09 17:43:52 -06:00
parent d2e184144b
commit fb105a2927
8 changed files with 665 additions and 89 deletions

View File

@@ -1,3 +1,5 @@
from pickle import FALSE
import app.bot.helper.jellyfinhelper as jelly
import discord
from discord.ext import commands
import asyncio
@@ -5,6 +7,7 @@ from plexapi.myplex import MyPlexAccount
from discord import Webhook, AsyncWebhookAdapter
import app.bot.helper.db as db
import app.bot.helper.plexhelper as plexhelper
import app.bot.helper.jellyfinhelper as jelly
import texttable
import os
from os import path
@@ -19,28 +22,91 @@ PLEXPASS = ""
PLEX_SERVER_NAME = ""
Plex_LIBS = None
USE_PLEX = False
plex_configured = True
jellyfin_configured = True
if(path.exists('app/config/config.ini')):
if(path.exists(CONFIG_PATH)):
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
# Get Plex config
try:
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
PLEXUSER = config.get(BOT_SECTION, 'plex_user')
PLEXPASS = config.get(BOT_SECTION, 'plex_pass')
PLEX_SERVER_NAME = config.get(BOT_SECTION, 'plex_server_name')
except:
pass
if(path.exists('app/config/config.ini')):
print("Could not load plex config")
plex_configured = False
# Get Plex roles config
try:
plex_roles = config.get(BOT_SECTION, 'plex_roles')
except:
pass
if(path.exists('app/config/config.ini')):
print("Could not get Plex roles config")
plex_roles = None
if plex_roles is not None:
plex_roles = list(plex_roles.split(','))
else:
plex_roles = []
# Get Plex libs config
try:
Plex_LIBS = config.get(BOT_SECTION, 'plex_libs')
except:
pass
if USE_PLEX:
print("Could not get Plex libs config. Defaulting to all libraries.")
Plex_LIBS = None
if Plex_LIBS is None:
Plex_LIBS = ["all"]
else:
Plex_LIBS = list(Plex_LIBS.split(','))
# Get Jellyfin config
try:
JELLYFIN_SERVER_URL = config.get(BOT_SECTION, 'jellyfin_server_url')
JELLYFIN_API_KEY = config.get(BOT_SECTION, "jellyfin_api_key")
except:
print("Could not load Jellyfin config")
jellyfin_configured = False
# Get Jellyfin roles config
try:
jellyfin_roles = config.get(BOT_SECTION, 'jellyfin_roles')
except:
print("Could not get Jellyfin roles config")
jellyfin_roles = None
if jellyfin_roles is not None:
jellyfin_roles = list(jellyfin_roles.split(','))
else:
jellyfin_roles = []
# Get Jellyfin libs config
try:
jellyfin_libs = config.get(BOT_SECTION, 'jellyfin_libs')
except:
print("Could not get Jellyfin libs config. Defaulting to all libraries.")
jellyfin_libs = None
if jellyfin_libs is None:
jellyfin_libs = ["all"]
else:
jellyfin_libs = list(jellyfin_libs.split(','))
# Get Enable config
try:
USE_JELLYFIN = config.get(BOT_SECTION, 'jellyfin_enabled')
USE_JELLYFIN = USE_JELLYFIN.lower() == "true"
except:
print("Could not get Jellyfin enable config. Defaulting to False")
USE_JELLYFIN = False
try:
USE_PLEX = config.get(BOT_SECTION, "plex_enabled")
USE_PLEX = USE_PLEX.lower() == "true"
except:
print("Could not get Plex enable config. Defaulting to False")
USE_PLEX = False
if USE_PLEX and plex_configured:
try:
account = MyPlexAccount(PLEXUSER, PLEXPASS)
plex = account.resource(PLEX_SERVER_NAME).connect() # returns a PlexServer instance
@@ -48,14 +114,9 @@ if USE_PLEX:
except Exception as e:
print('Error with plex login. Please check username and password and Plex server name or setup plex in the bot.')
print(f'Error: {e}')
if plex_roles is not None:
plex_roles = list(plex_roles.split(','))
if Plex_LIBS is None:
Plex_LIBS = ["all"]
else:
Plex_LIBS = list(Plex_LIBS.split(','))
print(f"Plex {'disabled' if not USE_PLEX else 'not configured'}. Skipping Plex login.")
class app(commands.Cog):
@@ -65,6 +126,7 @@ class app(commands.Cog):
@commands.Cog.listener()
async def on_ready(self):
print('Made by Sleepingpirate https://github.com/Sleepingpirates/')
print('Jellyfin implementation by Yoruio https://github.com/Yoruio/')
print(f'Logged in as {self.bot.user} (ID: {self.bot.user.id})')
print('------')
if plex_roles is None:
@@ -77,6 +139,12 @@ class app(commands.Cog):
async def embedinfo(self, author, message):
embed1 = discord.Embed(title=message, color=0x00F500)
await author.send(embed=embed1)
async def embedcustom(self, recipient, title, fields):
embed = discord.Embed(title=title)
for k in fields:
embed.add_field(name=str(k), value=str(fields[k]), inline=True)
await recipient.send(embed=embed)
async def getemail(self, after):
email = None
@@ -98,6 +166,31 @@ class app(commands.Cog):
message = "Timed Out. Message Server Admin with your email so They Can Add You Manually."
await self.embederror(after, message)
return None
async def getusername(self, after):
username = None
await self.embedinfo(after, f"Welcome To Jellyfin! Just reply with a username for Jellyfin so we can add you!")
await self.embedinfo(after, f"I will wait 24 hours for your message, if you do not send it by then I will cancel the command.")
while (username is None):
def check(m):
return m.author == after and not m.guild
try:
username = await self.bot.wait_for('message', timeout=86400, check=check)
if(jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, str(username.content))):
return str(username.content)
else:
username = None
message = "This username is already choosen. Please select another Username."
await self.embederror(after, message)
continue
except asyncio.TimeoutError:
message = "Timed Out. Message Server Admin with your preferred username so They Can Add You Manually."
await self.embederror(after, message)
return None
except Exception as e:
await self.embederror(after, "Something went wrong. Please try again with another username.")
print (e)
username = None
async def addtoplex(self, email, channel):
@@ -123,6 +216,30 @@ class app(commands.Cog):
else:
await self.embederror(channel, 'Invalid email.')
return False
async def addtojellyfin(self, username, password, channel):
if not jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username):
await self.embederror(channel, f'An account with username {username} already exists.')
return
if jelly.add_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username, password, jellyfin_libs):
await self.embedinfo(channel, 'User successfully added to Jellyfin')
return True
else:
await self.embederror(channel, 'There was an error adding this user to Jellyfin. Check logs for more info.')
return False
async def removefromjellyfin(self, username, channel):
if jelly.verify_username(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username):
await self.embederror(channel, f'Could not find account with username {username}.')
return
if jelly.remove_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username):
await self.embedinfo(channel, f'Successfully removed user {username} from Jellyfin.')
return True
else:
await self.embederror(channel, f'There was an error removing this user from Jellyfin. Check logs for more info.')
return False
@commands.Cog.listener()
async def on_member_update(self, before, after):
@@ -130,38 +247,95 @@ class app(commands.Cog):
return
roles_in_guild = after.guild.roles
role = None
for role_for_app in plex_roles:
for role_in_guild in roles_in_guild:
if role_in_guild.name == role_for_app:
role = role_in_guild
if role is not None and (role in after.roles and role not in before.roles):
email = await self.getemail(after)
if email is not None:
await self.embedinfo(after, "Got it we will be adding your email to plex shortly!")
if plexhelper.plexadd(plex,email,Plex_LIBS):
db.save_user(str(after.id), email)
await asyncio.sleep(5)
await self.embedinfo(after, 'You have Been Added To Plex! Login to plex and accept the invite!')
else:
await self.embedinfo(after, 'There was an error adding this email address. Message Server Admin.')
return
plex_processed = False
jellyfin_processed = False
elif role is not None and (role not in after.roles and role in before.roles):
try:
user_id = after.id
email = db.get_useremail(user_id)
plexhelper.plexremove(plex,email)
deleted = db.delete_user(user_id)
if deleted:
print("Removed {} from db".format(email))
#await secure.send(plexname + ' ' + after.mention + ' was removed from plex')
else:
print("Cannot remove this user from db.")
except Exception as e:
print(e)
print("{} Cannot remove this user from plex.".format(email))
return
# Check Plex roles
if plex_configured and USE_PLEX:
for role_for_app in plex_roles:
for role_in_guild in roles_in_guild:
if role_in_guild.name == role_for_app:
role = role_in_guild
# Plex role was added
if role is not None and (role in after.roles and role not in before.roles):
email = await self.getemail(after)
if email is not None:
await self.embedinfo(after, "Got it we will be adding your email to plex shortly!")
if plexhelper.plexadd(plex,email,Plex_LIBS):
db.save_user_email(str(after.id), email)
await asyncio.sleep(5)
await self.embedinfo(after, 'You have Been Added To Plex! Login to plex and accept the invite!')
else:
await self.embedinfo(after, 'There was an error adding this email address. Message Server Admin.')
plex_processed = True
break
# Plex role was removed
elif role is not None and (role not in after.roles and role in before.roles):
try:
user_id = after.id
email = db.get_useremail(user_id)
plexhelper.plexremove(plex,email)
deleted = db.remove_email(user_id)
if deleted:
print("Removed Plex email {} from db".format(after.name))
#await secure.send(plexname + ' ' + after.mention + ' was removed from plex')
else:
print("Cannot remove Plex from this user.")
await self.embedinfo(after, "You have been removed from Plex")
except Exception as e:
print(e)
print("{} Cannot remove this user from plex.".format(email))
plex_processed = True
break
if plex_processed:
break
# Check Jellyfin roles
if jellyfin_configured and USE_JELLYFIN:
for role_for_app in jellyfin_roles:
for role_in_guild in roles_in_guild:
if role_in_guild.name == role_for_app:
role = role_in_guild
# Jellyfin role was added
if role is not None and (role in after.roles and role not in before.roles):
username = await self.getusername(after)
if username is not None:
await self.embedinfo(after, "Got it we will be creating your Jellyfin account shortly!")
password = jelly.generate_password(16)
if jelly.add_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username, password, jellyfin_libs):
db.save_user_jellyfin(str(after.id), username)
await asyncio.sleep(5)
await self.embedcustom(after, "You have been added to Jellyfin!", {'Username': username, 'Password': f"||{password}||"})
await self.embedinfo(after, f"Go to {JELLYFIN_SERVER_URL} to log in!")
else:
await self.embedinfo(after, 'There was an error adding this user to Jellyfin. Message Server Admin.')
jellyfin_processed = True
break
# Jellyfin role was removed
elif role is not None and (role not in after.roles and role in before.roles):
try:
user_id = after.id
username = db.get_jellyfin_username(user_id)
jelly.remove_user(JELLYFIN_SERVER_URL, JELLYFIN_API_KEY, username)
deleted = db.remove_jellyfin(user_id)
if deleted:
print("Removed Jellyfin from {}".format(after.name))
#await secure.send(plexname + ' ' + after.mention + ' was removed from plex')
else:
print("Cannot remove Jellyfin from this user")
await self.embedinfo(after, "You have been removed from Jellyfin")
except Exception as e:
print(e)
print("{} Cannot remove this user from Jellyfin.".format(username))
jellyfin_processed = True
break
if jellyfin_processed:
break
@commands.Cog.listener()
async def on_member_remove(self, member):
@@ -181,6 +355,18 @@ class app(commands.Cog):
async def plexremove(self, ctx, email):
await self.removefromplex(email, ctx.channel)
@commands.has_permissions(administrator=True)
@commands.command(aliases=['jellyadd'])
async def jellyfininvite(self, ctx, username):
password = jelly.generate_password(16)
if await self.addtojellyfin(username, password, ctx.channel):
await self.embedcustom(ctx.author, "Jellyfin user created!", {'Username': username, 'Password': f"||{password}||"})
@commands.has_permissions(administrator=True)
@commands.command(aliases=['jellyrm'])
async def jellyfinremove(self, ctx, username):
await self.removefromjellyfin(username, ctx.channel)
@commands.has_permissions(administrator=True)
@commands.command()
async def dbadd(self, ctx, member: discord.Member, email, jellyfin_username):
@@ -240,32 +426,34 @@ class app(commands.Cog):
@commands.command()
async def dbrm(self, ctx, position):
embed = discord.Embed(title='Invitarr Database.')
all = db.read_useremail() # TODO: no need to read from DB or make a table here.
all = db.read_useremail()
table = texttable.Texttable()
table.set_cols_dtype(["t", "t", "t"])
table.set_cols_align(["c", "c", "c"])
header = ("#", "Name", "Email")
table.set_cols_dtype(["t", "t", "t", "t"])
table.set_cols_align(["c", "c", "c", "c"])
header = ("#", "Name", "Email", "Jellyfin")
table.add_row(header)
for index, peoples in enumerate(all):
index = index + 1
id = int(peoples[1])
dbuser = self.bot.get_user(id)
dbemail = peoples[2]
dbemail = peoples[2] if peoples[2] else "No Plex"
dbjellyfin = peoples[3] if peoples[3] else "No Jellyfin"
try:
username = dbuser.name
except:
username = "User Not Found."
embed.add_field(name=f"**{index}. {username}**", value=dbemail+'\n', inline=False)
table.add_row((index, username, dbemail))
embed.add_field(name=f"**{index}. {username}**", value=dbemail+'\n'+dbjellyfin+'\n', inline=False)
table.add_row((index, username, dbemail, dbjellyfin))
try:
position = int(position) - 1
id = all[position][1]
email = db.get_useremail(id)
discord_user = await self.bot.fetch_user(id)
username = discord_user.name
deleted = db.delete_user(id)
if deleted:
print("Removed {} from db".format(email))
await self.embedinfo(ctx.channel,"Removed {} from db".format(email))
print("Removed {} from db".format(username))
await self.embedinfo(ctx.channel,"Removed {} from db".format(username))
else:
await self.embederror(ctx.channel,"Cannot remove this user from db.")
except Exception as e:

View File

@@ -8,7 +8,8 @@ config = configparser.ConfigParser()
CONFIG_KEYS = ['username', 'password', 'discord_bot_token', 'plex_user', 'plex_pass',
'plex_roles', 'plex_server_name', 'plex_libs', 'owner_id', 'channel_id',
'auto_remove_user']
'auto_remove_user', 'jellyfin_api_key', 'jellyfin_server_url', 'jellyfin_roles',
'jellyfin_libs', 'plex_enabled', 'jellyfin_enabled']
# settings
Discord_bot_token = ""
@@ -17,6 +18,11 @@ PLEXUSER = ""
PLEXPASS = ""
PLEX_SERVER_NAME = ""
Plex_LIBS = None
JELLYFIN_SERVER_URL = ""
JELLYFIN_API_KEY = ""
jellyfin_libs = ""
jellyfin_roles = None
switch = 0
@@ -36,26 +42,63 @@ try:
except Exception as e:
pass
if(path.exists('app/config/config.ini')):
if(path.exists(CONFIG_PATH)):
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
# Get Plex config
try:
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
PLEXUSER = config.get(BOT_SECTION, 'plex_user')
PLEXPASS = config.get(BOT_SECTION, 'plex_pass')
PLEX_SERVER_NAME = config.get(BOT_SECTION, 'plex_server_name')
except:
pass
print("Could not load plex config")
if(path.exists('app/config/config.ini')):
# Get Plex roles config
try:
roles = config.get(BOT_SECTION, 'plex_roles')
plex_roles = config.get(BOT_SECTION, 'plex_roles')
except:
pass
if(path.exists('app/config/config.ini')):
print("Could not get Plex roles config")
# Get Plex libs config
try:
Plex_LIBS = config.get(BOT_SECTION, 'plex_libs')
except:
pass
print("Could not get Plex libs config")
# Get Jellyfin config
try:
JELLYFIN_SERVER_URL = config.get(BOT_SECTION, 'jellyfin_server_url')
JELLYFIN_API_KEY = config.get(BOT_SECTION, "jellyfin_api_key")
except:
print("Could not load Jellyfin config")
# Get Jellyfin roles config
try:
jellyfin_roles = config.get(BOT_SECTION, 'jellyfin_roles')
except:
print("Could not get Jellyfin roles config")
# Get Jellyfin libs config
try:
jellyfin_libs = config.get(BOT_SECTION, 'jellyfin_libs')
except:
print("Could not get Jellyfin libs config")
# Get Enable config
try:
USE_JELLYFIN = config.get(BOT_SECTION, 'jellyfin_enabled')
except:
print("Could not get Jellyfin enable config. Defaulting to False")
USE_Jellyfin = False
try:
USE_PLEX = config.get(BOT_SECTION, "plex_enabled")
except:
print("Could not get Plex enable config. Defaulting to False")
USE_PLEX = False
def get_config():
"""
Function to return current config

View File

@@ -94,7 +94,7 @@ def get_useremail(username):
if email:
return email
else:
return "No users found"
return "No email found"
except:
return "error in fetching from db"
else:
@@ -122,6 +122,32 @@ def get_jellyfin_username(username):
else:
return "username cannot be empty"
def remove_email(username):
"""
Sets email of discord user to null in database
"""
if username:
conn.execute(f"UPDATE clients SET email = null WHERE discord_username = '{username}'")
conn.commit()
print(f"Email removed from user {username} in database")
return True
else:
print(f"Username cannot be empty.")
return False
def remove_jellyfin(username):
"""
Sets jellyfin username of discord user to null in database
"""
if username:
conn.execute(f"UPDATE clients SET jellyfin_username = null WHERE discord_username = '{username}'")
conn.commit()
print(f"Jellyfin username removed from user {username} in database")
return True
else:
print(f"Username cannot be empty.")
return False
def delete_user(username):
if username:

View File

@@ -0,0 +1,175 @@
import requests
import random
import string
def add_user(jellyfin_url, jellyfin_api_key, username, password, jellyfin_libs):
try:
url = f"{jellyfin_url}/Users/New"
querystring = {"api_key":jellyfin_api_key}
payload = {
"Name": username,
"Password": password
}
headers = {"Content-Type": "application/json"}
response = requests.request("POST", url, json=payload, headers=headers, params=querystring)
userId = response.json()["Id"]
if response.status_code != 200:
print(f"Error creating new Jellyfin user: {response.text}")
return False
# Grant access to User
url = f"{jellyfin_url}/Users/{userId}/Policy"
querystring = {"api_key":jellyfin_api_key}
enabled_folders = []
server_libs = get_libraries(jellyfin_url, jellyfin_api_key)
if jellyfin_libs[0] != "all":
for lib in jellyfin_libs:
found = False
for server_lib in server_libs:
if lib == server_lib['Name']:
enabled_folders.append(server_lib['ItemId'])
found = True
if not found:
print(f"Couldn't find Jellyfin Library: {lib}")
payload = {
"IsAdministrator": False,
"IsHidden": True,
"IsDisabled": False,
"BlockedTags": [],
"EnableUserPreferenceAccess": True,
"AccessSchedules": [],
"BlockUnratedItems": [],
"EnableRemoteControlOfOtherUsers": False,
"EnableSharedDeviceControl": True,
"EnableRemoteAccess": True,
"EnableLiveTvManagement": True,
"EnableLiveTvAccess": True,
"EnableMediaPlayback": True,
"EnableAudioPlaybackTranscoding": True,
"EnableVideoPlaybackTranscoding": True,
"EnablePlaybackRemuxing": True,
"ForceRemoteSourceTranscoding": False,
"EnableContentDeletion": False,
"EnableContentDeletionFromFolders": [],
"EnableContentDownloading": True,
"EnableSyncTranscoding": True,
"EnableMediaConversion": True,
"EnabledDevices": [],
"EnableAllDevices": True,
"EnabledChannels": [],
"EnableAllChannels": False,
"EnabledFolders": enabled_folders,
"EnableAllFolders": jellyfin_libs[0] == "all",
"InvalidLoginAttemptCount": 0,
"LoginAttemptsBeforeLockout": -1,
"MaxActiveSessions": 0,
"EnablePublicSharing": True,
"BlockedMediaFolders": [],
"BlockedChannels": [],
"RemoteClientBitrateLimit": 0,
"AuthenticationProviderId": "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider",
"PasswordResetProviderId": "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider",
"SyncPlayAccess": "CreateAndJoinGroups"
}
headers = {"content-type": "application/json"}
response = requests.request("POST", url, json=payload, headers=headers, params=querystring)
if response.status_code == 200 or response.status_code == 204:
return True
else:
print(f"Error setting user permissions: {response.text}")
except Exception as e:
print(e)
return False
def get_libraries(jellyfin_url, jellyfin_api_key):
url = f"{jellyfin_url}/Library/VirtualFolders"
querystring = {"api_key":jellyfin_api_key}
response = requests.request("GET", url, params=querystring)
return response.json()
def verify_username(jellyfin_url, jellyfin_api_key, username):
users = get_users(jellyfin_url, jellyfin_api_key)
valid = True
for user in users:
if user['Name'] == username:
valid = False
break
return valid
def remove_user(jellyfin_url, jellyfin_api_key, jellyfin_username):
try:
# Get User ID
users = get_users(jellyfin_url, jellyfin_api_key)
userId = None
for user in users:
if user['Name'].lower() == jellyfin_username.lower():
userId = user['Id']
if userId is None:
# User not found
print(f"Error removing user {jellyfin_username} from Jellyfin: Could not find user.")
return False
# Delete User
url = f"{jellyfin_url}/Users/{userId}"
querystring = {"api_key":jellyfin_api_key}
response = requests.request("DELETE", url, params=querystring)
if response.status_code == 204 or response.status_code == 200:
return True
else:
print(f"Error deleting Jellyfin user: {response.text}")
except Exception as e:
print(e)
return False
def get_users(jellyfin_url, jellyfin_api_key):
url = f"{jellyfin_url}/Users"
querystring = {"api_key":jellyfin_api_key}
response = requests.request("GET", url, params=querystring)
return response.json()
def generate_password(length, lower=True, upper=True, numbers=True, symbols=True):
character_list = []
if not (lower or upper or numbers or symbols):
raise ValueError("At least one character type must be provided")
if lower:
character_list += string.ascii_lowercase
if upper:
character_list += string.ascii_uppercase
if numbers:
character_list += string.digits
if symbols:
character_list += string.punctuation
return "".join(random.choice(character_list) for i in range(length))
def get_config(jellyfin_url, jellyfin_api_key):
url = f"{jellyfin_url}/System/Configuration"
querystring = {"api_key":jellyfin_api_key}
response = requests.request("GET", url, params=querystring)
return response.json()
def get_status(jellyfin_url, jellyfin_api_key):
url = f"{jellyfin_url}/System/Configuration"
querystring = {"api_key":jellyfin_api_key}
response = requests.request("GET", url, params=querystring)
return response.status_code

View File

@@ -46,7 +46,4 @@ def plexremoveinvite(plex, plexname):
def verifyemail(addressToVerify):
regex = '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$'
match = re.match(regex, addressToVerify.lower())
if match == None:
return False
else:
return True
return match != None